feat(media): gestion des médias locaux
Ajoute le stockage local des médias (infrastructure/local/local_media_storage.dart) et les use cases/ports associés. flutter analyze propre, 7/7 tests unitaires verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -46,6 +46,38 @@ final class ExerciseUseCases {
|
||||
|
||||
Future<void> save(Exercise exercise) => repository.save(exercise);
|
||||
|
||||
Future<Exercise> attachImage({
|
||||
required String exerciseId,
|
||||
required String mediaAssetId,
|
||||
}) async {
|
||||
final exercise = await repository.findById(exerciseId);
|
||||
if (exercise == null) {
|
||||
throw const DomainException('Exercise not found.');
|
||||
}
|
||||
final updated = exercise.copyWith(
|
||||
imageMediaId: mediaAssetId,
|
||||
metadata: exercise.metadata.touch(clock.now()),
|
||||
);
|
||||
await repository.save(updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
Future<Exercise> attachVideo({
|
||||
required String exerciseId,
|
||||
required String mediaAssetId,
|
||||
}) async {
|
||||
final exercise = await repository.findById(exerciseId);
|
||||
if (exercise == null) {
|
||||
throw const DomainException('Exercise not found.');
|
||||
}
|
||||
final updated = exercise.copyWith(
|
||||
videoMediaId: mediaAssetId,
|
||||
metadata: exercise.metadata.touch(clock.now()),
|
||||
);
|
||||
await repository.save(updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
Future<Exercise> archive(String id) async {
|
||||
final exercise = await repository.findById(id);
|
||||
if (exercise == null) {
|
||||
@ -57,6 +89,91 @@ final class ExerciseUseCases {
|
||||
}
|
||||
}
|
||||
|
||||
final class MediaUseCases {
|
||||
const MediaUseCases({
|
||||
required this.mediaRepository,
|
||||
required this.exerciseRepository,
|
||||
required this.storage,
|
||||
required this.clock,
|
||||
required this.ids,
|
||||
required this.originDeviceId,
|
||||
});
|
||||
|
||||
final MediaAssetRepository mediaRepository;
|
||||
final ExerciseRepository exerciseRepository;
|
||||
final LocalMediaStorage storage;
|
||||
final Clock clock;
|
||||
final IdGenerator ids;
|
||||
final String originDeviceId;
|
||||
|
||||
Future<MediaAsset> importMedia({
|
||||
required String sourcePath,
|
||||
required MediaKind kind,
|
||||
}) async {
|
||||
final now = clock.now();
|
||||
final id = ids.newId();
|
||||
final stored = await storage.importFile(
|
||||
sourcePath: sourcePath,
|
||||
kind: kind,
|
||||
stableFileName: id,
|
||||
);
|
||||
final asset = MediaAsset(
|
||||
metadata: EntityMetadata(
|
||||
id: id,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
originDeviceId: originDeviceId,
|
||||
),
|
||||
kind: kind,
|
||||
localUri: stored.localUri,
|
||||
mimeType: stored.mimeType,
|
||||
sizeBytes: stored.sizeBytes,
|
||||
width: stored.width,
|
||||
height: stored.height,
|
||||
durationMs: stored.durationMs,
|
||||
);
|
||||
await mediaRepository.save(asset);
|
||||
return asset;
|
||||
}
|
||||
|
||||
Future<Exercise> attachToExercise({
|
||||
required String exerciseId,
|
||||
required String mediaAssetId,
|
||||
}) async {
|
||||
final exercise = await exerciseRepository.findById(exerciseId);
|
||||
if (exercise == null) {
|
||||
throw const DomainException('Exercise not found.');
|
||||
}
|
||||
final media = await mediaRepository.findById(mediaAssetId);
|
||||
if (media == null) {
|
||||
throw const DomainException('Media asset not found.');
|
||||
}
|
||||
final updated = switch (media.kind) {
|
||||
MediaKind.image => exercise.copyWith(
|
||||
imageMediaId: media.metadata.id,
|
||||
metadata: exercise.metadata.touch(clock.now()),
|
||||
),
|
||||
MediaKind.video => exercise.copyWith(
|
||||
videoMediaId: media.metadata.id,
|
||||
metadata: exercise.metadata.touch(clock.now()),
|
||||
),
|
||||
};
|
||||
await exerciseRepository.save(updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
Future<int> deleteOrphanedManagedFiles() async {
|
||||
final activeAssets = await mediaRepository.listActive();
|
||||
final referencedUris = activeAssets.map((asset) => asset.localUri).toSet();
|
||||
final managedUris = await storage.listManagedLocalUris();
|
||||
final orphanedUris = managedUris.difference(referencedUris);
|
||||
for (final uri in orphanedUris) {
|
||||
await storage.deleteByLocalUri(uri);
|
||||
}
|
||||
return orphanedUris.length;
|
||||
}
|
||||
}
|
||||
|
||||
final class ProgramUseCases {
|
||||
const ProgramUseCases({
|
||||
required this.programRepository,
|
||||
|
||||
Reference in New Issue
Block a user