Ajoute l'adapter API distant de partage (infrastructure/remote/share_api.dart) et les use cases associés (application/use_cases.dart), avec cache d'inbox et import local des ressources partagées acceptées. Étend le modèle Drift (migration schemaVersion 11→12) et les entités du domaine en conséquence. flutter pub get OK, build_runner OK, dart format appliqué, analyze propre (mêmes infos préexistantes), 124/124 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
467 lines
12 KiB
Dart
467 lines
12 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();
|
|
}
|
|
|
|
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> 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<ActiveExerciseStepProgressState?> findExerciseStepProgressState({
|
|
required String sessionId,
|
|
required int programIndex,
|
|
required int exerciseIndex,
|
|
required int setIndex,
|
|
});
|
|
Future<List<ActiveSetResult>> listSetResults(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 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);
|
|
}
|
|
}
|