Files
GameTime/lib/application/ports.dart
Blomios ac51c16b51 feat(exécution): dernière performance et meilleur score par exercice (ticket #81)
Ajoute la migration Drift v17 (colonne sourceExerciseIdSnapshot sur
workout_history_set_results et workout_history_step_results, index
associés), le port ExercisePerformanceReferenceRepository et son
implémentation Drift, et le use case
ExercisePerformanceReferenceUseCase (dernière performance + meilleur
score par métrique active pour un exercice donné).

Validé GO par Main : dart analyze propre (1 lint mineur de style),
178 tests (4 échecs préexistants sans rapport avec ce lot).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 07:54:49 +02:00

597 lines
16 KiB
Dart

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;
}
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,
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<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> 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> 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<ActiveRestState?> findRestStateById(String id);
Future<ActiveScoreStopwatchState?> findScoreStopwatchState({
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,
);
}
abstract interface class WorkoutHistoryRepository {
Future<WorkoutHistory?> findById(String id);
Future<List<WorkoutHistory>> listActive();
Future<void> save(WorkoutHistory history);
Future<void> saveSetResult(WorkoutHistorySetResult result);
Future<void> saveStepResult(WorkoutHistoryStepResult result);
Future<void> delete(String id, DateTime deletedAt);
}
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);
}
}