Files
GameTime/lib/application/ports.dart
Blomios fe3608cc4a feat(server): implemente synchronisation et serveur avec fixtures (#187)
- Implémente la couche de synchronisation avec le serveur
- Ajoute les fixtures versionnées pour les tests
- Met à jour Drift database et repositories pour le support sync
- Améliore les tests de synchronisation
- Corrige et améliore le watch companion pour la collecte de métriques

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 23:07:31 +02:00

1098 lines
29 KiB
Dart

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<Exercise> 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<String, Object?> payload;
LocalBackupResource copyWith({Map<String, Object?>? 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<LocalBackupResource> mediaAssets;
final List<LocalBackupResource> exercises;
final List<LocalBackupResource> programs;
final List<LocalBackupResource> workoutTemplates;
final List<LocalBackupResource> workoutHistories;
final List<EmbeddedBackupMediaFile> 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<LocalBackupResource>? mediaAssets,
List<EmbeddedBackupMediaFile>? 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<LocalDataExportSnapshot> readExportSnapshot(DateTime exportedAt);
Future<bool> hasOpenActiveWorkoutSession();
Future<bool> hasAnyUserData();
Future<LocalBackupImportResult> applyImportSnapshot({
required LocalDataExportSnapshot snapshot,
required LocalBackupImportMode mode,
required DateTime importedAt,
});
}
abstract interface class LocalBackupMediaStore {
Future<List<EmbeddedBackupMediaFile>> readEmbeddableFiles(
List<LocalBackupResource> mediaAssets,
);
Future<Map<String, RestoredMediaFile>> restoreEmbeddedFiles(
List<EmbeddedBackupMediaFile> 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<DateTime> 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<ProgressionPointData> 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<ProgressionGlobalStatsData> readGlobalStats(
ProgressionDateRange range,
);
Future<List<ProgressionExerciseOptionData>> listExerciseOptions(
ProgressionDateRange range,
);
Future<List<ProgressionMeasureOptionData>> listMeasureOptions({
required ProgressionDateRange range,
required String exerciseKey,
});
Future<ProgressionExerciseSeriesData> 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<ProgressionExerciseOption> 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<ProgressionMeasureOption> 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<ProgressionPoint> 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<int> readAppliedStarterSeedVersion();
Future<void> writeAppliedStarterSeedVersion(int version, DateTime appliedAt);
}
abstract interface class StarterContentRepository {
Future<bool> isLocalContentEmpty();
Future<void> 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<void> saveToken(String token, DateTime expiresAt);
Future<String?> readToken();
Future<void> clearToken();
}
abstract interface class OnlineAccountRepository {
Future<UserAccountSession?> currentSession();
Future<void> saveSession(UserAccountSession session);
Future<void> clearSession();
}
abstract interface class RemoteAuthApi {
Future<RemoteAuthResult> register({
required String email,
required String password,
String? displayName,
});
Future<RemoteAuthResult> login({
required String email,
required String password,
});
Future<void> 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<String, Object?> 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<RemoteSyncPushItemResult> 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<String, Object?> payload;
}
final class RemoteSyncPullResult {
const RemoteSyncPullResult({required this.serverCursor, required this.items});
final String? serverCursor;
final List<RemoteSyncedItem> items;
}
abstract interface class RemoteSyncApi {
Future<RemoteSyncPushResult> push({
required String deviceId,
required List<RemoteSyncPushItem> items,
required String token,
});
Future<RemoteSyncPullResult> 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<String> changeLogIds;
final RemoteSyncPushItem item;
}
abstract interface class SyncMetadataRepository {
Future<SyncMetadataSnapshot> read();
Future<void> save(SyncMetadataSnapshot metadata);
}
abstract interface class RemoteResourceMappingRepository {
Future<RemoteResourceMapping?> find({
required SyncResourceType resourceType,
required String clientId,
});
Future<void> save(RemoteResourceMapping mapping);
}
abstract interface class LocalSyncChangeRepository {
Future<List<PendingSyncChange>> listPendingChanges();
Future<void> markChangesSynced(List<String> changeLogIds, DateTime syncedAt);
Future<bool> applyRemoteItem(RemoteSyncedItem item);
}
final class RemoteShareSendResult {
const RemoteShareSendResult({
required this.shareId,
required this.recipientUserIds,
required this.unresolvedEmails,
});
final String shareId;
final List<String> recipientUserIds;
final List<String> 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<String> recipientUserIds;
final List<String> unresolvedEmails;
final String? pendingActionId;
}
abstract interface class RemoteShareApi {
Future<RemoteShareSendResult> sendShare({
required ShareResourceType resourceType,
required Map<String, Object?> payload,
required List<String> recipientEmails,
required String token,
});
Future<List<ShareInboxItem>> fetchInbox(String token);
Future<List<RemoteSyncedItem>> acceptShare(String shareId, String token);
Future<void> declineShare(String shareId, String token);
Future<void> revokeShare(String shareId, String token);
}
abstract interface class ShareInboxRepository {
Future<List<ShareInboxItem>> listAll();
Future<ShareInboxItem?> findByShareId(String shareId);
Future<void> upsert(ShareInboxItem item);
Future<void> upsertAll(List<ShareInboxItem> items);
Future<void> markStatus(
String shareId,
ShareInboxStatus status,
DateTime respondedAt,
);
}
abstract interface class PendingShareActionRepository {
Future<List<PendingShareAction>> listPending();
Future<void> add(PendingShareAction action);
Future<void> markSucceeded(String id, DateTime attemptedAt);
Future<void> markFailed(String id, DateTime attemptedAt);
}
const Object _portsUnchanged = Object();
abstract interface class ExerciseRepository {
Future<Exercise?> findById(String id);
Future<List<Exercise>> listActive();
Future<bool> isReferencedByProgram(String id);
Future<void> 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<StoredMediaFile> importFile({
required String sourcePath,
required MediaKind kind,
required String stableFileName,
});
Future<Set<String>> listManagedLocalUris();
Future<void> deleteByLocalUri(String localUri);
}
abstract interface class MediaAssetRepository {
Future<MediaAsset?> findById(String id);
Future<List<MediaAsset>> listActive();
Future<void> save(MediaAsset mediaAsset);
}
abstract interface class ProgramRepository {
Future<Program?> findById(String id);
Future<List<Program>> listActive();
Future<void> save(Program program);
Future<void> saveExercise(ProgramExercise exercise);
Future<void> replaceExercises(Program program, DateTime deletedAt);
}
abstract interface class WorkoutTemplateRepository {
Future<WorkoutTemplate?> findById(String id);
Future<List<WorkoutTemplate>> listActive();
Future<void> save(WorkoutTemplate template);
Future<void> saveAll(List<WorkoutTemplate> templates);
Future<void> saveProgram(WorkoutTemplateProgram program);
Future<void> saveOverride(WorkoutTemplateExerciseOverride override);
Future<void> replaceComposition(WorkoutTemplate template, DateTime deletedAt);
}
abstract interface class ActiveSessionRepository {
Future<ActiveWorkoutSession?> findById(String id);
Future<ActiveWorkoutSession?> findOpen();
Future<void> save(ActiveWorkoutSession session);
Future<void> saveSetResult(ActiveSetResult result);
Future<void> saveSetTimerState(ActiveSetTimerState state);
Future<void> saveRestState(ActiveRestState restState);
Future<void> saveScoreStopwatchState(ActiveScoreStopwatchState state);
Future<void> saveManualScoreState(ActiveManualScoreState state);
Future<void> saveExerciseStepProgressState(
ActiveExerciseStepProgressState state,
);
Future<void> saveExerciseStepResult(ActiveExerciseStepResult result);
Future<void> deleteScoreStopwatchState({
required String sessionId,
required int programIndex,
required int exerciseIndex,
required int setIndex,
required DateTime deletedAt,
});
Future<void> deleteManualScoreState({
required String sessionId,
required int programIndex,
required int exerciseIndex,
required int setIndex,
required DateTime deletedAt,
});
Future<ActiveRestState?> findRestStateById(String id);
Future<ActiveScoreStopwatchState?> findScoreStopwatchState({
required String sessionId,
required int programIndex,
required int exerciseIndex,
required int setIndex,
});
Future<ActiveManualScoreState?> findManualScoreState({
required String sessionId,
required int programIndex,
required int exerciseIndex,
required int setIndex,
});
Future<ActiveSetTimerState?> findSetTimerState({
required String sessionId,
required int programIndex,
required int exerciseIndex,
required int setIndex,
});
Future<ActiveExerciseStepProgressState?> findExerciseStepProgressState({
required String sessionId,
required int programIndex,
required int exerciseIndex,
required int setIndex,
});
Future<List<ActiveSetResult>> listSetResults(String sessionId);
Future<List<ActiveSetTimerState>> listSetTimerStates(String sessionId);
Future<List<ActiveExerciseStepProgressState>> listExerciseStepProgressStates(
String sessionId,
);
Future<List<ActiveExerciseStepResult>> listExerciseStepResults(
String sessionId,
);
Future<List<ActiveRestState>> listRestStates(String sessionId);
Future<List<ActiveScoreStopwatchState>> listScoreStopwatchStates(
String sessionId,
);
Future<List<ActiveManualScoreState>> listManualScoreStates(String sessionId);
}
abstract interface class WorkoutHistoryRepository {
Future<WorkoutHistory?> findById(String id);
Future<List<WorkoutHistory>> listActive();
Future<void> save(WorkoutHistory history);
Future<void> patchHeartRateSummary({
required String historyId,
int? minHeartRateBpm,
required double averageHeartRateBpm,
required int maxHeartRateBpm,
double? totalDistanceMeters,
double? totalCaloriesKcal,
required DateTime patchedAt,
});
Future<void> saveSetResult(WorkoutHistorySetResult result);
Future<void> saveStepResult(WorkoutHistoryStepResult result);
Future<void> delete(String id, DateTime deletedAt);
}
abstract interface class WorkoutTelemetryRepository {
Future<bool> saveSample(WorkoutTelemetrySample sample);
Future<ActiveWorkoutTelemetryWindowState?> findWindowState(String sessionId);
Future<void> saveWindowState(ActiveWorkoutTelemetryWindowState state);
Future<void> deleteWindowState(String sessionId);
Future<List<WorkoutTelemetrySample>> listSamples(String sessionId);
Future<List<WorkoutTelemetrySample>> listSamplesForScope({
required String sessionId,
required WorkoutTelemetryAggregateScope scope,
int? programIndex,
int? exerciseIndex,
int? setIndex,
int? passageIndex,
int? stepIndex,
});
Future<void> replaceAggregatesForSession({
required String sessionId,
required List<WorkoutTelemetryAggregate> aggregates,
});
Future<List<WorkoutTelemetryAggregate>> listAggregates(String sessionId);
Future<WorkoutTelemetryAggregate?> 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<bool> hasAnyCompletedHistoryForExercise(String exerciseId);
Future<WorkoutHistorySetPerformance?> findLatestSetPerformance({
required String exerciseId,
required ActivePerformanceMeasures activeMeasures,
required int currentSetIndex,
});
Future<WorkoutHistoryMetricPerformance?> 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<SyncRunSummary> synchronize();
}
final class NoOpSyncGateway implements SyncGateway {
const NoOpSyncGateway();
@override
Future<SyncRunSummary> synchronize() async {
return const SyncRunSummary(pushedChanges: 0, pulledChanges: 0);
}
}