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:
2026-07-17 18:31:28 +02:00
parent 2499a95e08
commit c77006f322
6 changed files with 591 additions and 0 deletions

View File

@ -35,6 +35,35 @@ final class DriftExerciseRepository implements ExerciseRepository {
}
}
final class DriftMediaAssetRepository implements MediaAssetRepository {
const DriftMediaAssetRepository(this.database);
final db.AppDatabase database;
@override
Future<domain.MediaAsset?> findById(String id) async {
final row = await (database.select(
database.mediaAssets,
)..where((table) => table.id.equals(id))).getSingleOrNull();
return row == null ? null : _mediaAssetFromRow(row);
}
@override
Future<List<domain.MediaAsset>> listActive() async {
final rows = await (database.select(
database.mediaAssets,
)..where((table) => table.deletedAt.isNull())).get();
return rows.map(_mediaAssetFromRow).toList();
}
@override
Future<void> save(domain.MediaAsset mediaAsset) async {
await database
.into(database.mediaAssets)
.insertOnConflictUpdate(_mediaAssetCompanion(mediaAsset));
}
}
final class DriftProgramRepository implements ProgramRepository {
const DriftProgramRepository(this.database);
@ -294,6 +323,49 @@ domain.Exercise _exerciseFromRow(db.Exercise row) {
);
}
db.MediaAssetsCompanion _mediaAssetCompanion(domain.MediaAsset asset) {
final values = _metadataValues(asset.metadata);
return db.MediaAssetsCompanion(
id: values[0] as Value<String>,
createdAt: values[1] as Value<DateTime>,
updatedAt: values[2] as Value<DateTime>,
deletedAt: values[3] as Value<DateTime?>,
schemaVersion: values[4] as Value<int>,
syncState: values[5] as Value<String>,
localRevision: values[6] as Value<int>,
originDeviceId: values[7] as Value<String>,
futureOwnerProfileId: values[8] as Value<String?>,
lastSyncedAt: values[9] as Value<DateTime?>,
remoteRevision: values[10] as Value<String?>,
kind: Value(_mediaKindToDb(asset.kind)),
localUri: Value(asset.localUri),
mimeType: Value(asset.mimeType),
sizeBytes: Value(asset.sizeBytes),
width: Value(asset.width),
height: Value(asset.height),
durationMs: Value(asset.durationMs),
checksum: Value(asset.checksum),
remoteUri: Value(asset.remoteUri),
thumbnailLocalUri: Value(asset.thumbnailLocalUri),
);
}
domain.MediaAsset _mediaAssetFromRow(db.MediaAsset row) {
return domain.MediaAsset(
metadata: _metadataFromRow(row),
kind: _mediaKindFromDb(row.kind),
localUri: row.localUri,
mimeType: row.mimeType,
sizeBytes: row.sizeBytes,
width: row.width,
height: row.height,
durationMs: row.durationMs,
checksum: row.checksum,
remoteUri: row.remoteUri,
thumbnailLocalUri: row.thumbnailLocalUri,
);
}
db.ProgramsCompanion _programCompanion(domain.Program program) {
final values = _metadataValues(program.metadata);
return db.ProgramsCompanion(
@ -741,6 +813,17 @@ String _activeStatusToDb(domain.ActiveWorkoutStatus status) => switch (status) {
domain.ActiveWorkoutStatus.abandoned => 'abandoned',
};
String _mediaKindToDb(domain.MediaKind kind) => switch (kind) {
domain.MediaKind.image => 'image',
domain.MediaKind.video => 'video',
};
domain.MediaKind _mediaKindFromDb(String value) => switch (value) {
'image' => domain.MediaKind.image,
'video' => domain.MediaKind.video,
_ => throw domain.DomainException('Unknown media kind: $value'),
};
domain.ActiveWorkoutStatus _activeStatusFromDb(String value) => switch (value) {
'running' => domain.ActiveWorkoutStatus.running,
'paused' => domain.ActiveWorkoutStatus.paused,