feat(exercice): galerie de 5 images par exercice (ticket #35)
Étend le modèle Drift (tables.dart, app_database.dart/.g.dart), les entités/use cases/repositories et exercise_library_screen.dart pour supporter jusqu'à 5 images par exercice au lieu d'une seule. flutter analyze propre, 63/63 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -14,7 +14,9 @@ final class DriftExerciseRepository implements ExerciseRepository {
|
||||
final row = await (database.select(
|
||||
database.exercises,
|
||||
)..where((table) => table.id.equals(id))).getSingleOrNull();
|
||||
return row == null ? null : _exerciseFromRow(row);
|
||||
return row == null
|
||||
? null
|
||||
: _exerciseFromRow(row, await _imageMediaIdsForExercise(id));
|
||||
}
|
||||
|
||||
@override
|
||||
@ -26,7 +28,13 @@ final class DriftExerciseRepository implements ExerciseRepository {
|
||||
)
|
||||
..orderBy([(table) => OrderingTerm.asc(table.name)]))
|
||||
.get();
|
||||
return rows.map(_exerciseFromRow).toList();
|
||||
final exercises = <domain.Exercise>[];
|
||||
for (final row in rows) {
|
||||
exercises.add(
|
||||
_exerciseFromRow(row, await _imageMediaIdsForExercise(row.id)),
|
||||
);
|
||||
}
|
||||
return exercises;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -45,15 +53,31 @@ final class DriftExerciseRepository implements ExerciseRepository {
|
||||
|
||||
@override
|
||||
Future<void> save(domain.Exercise exercise) async {
|
||||
await _upsertWithChangeLog(
|
||||
database: database,
|
||||
tableName: 'exercises',
|
||||
entityType: 'Exercise',
|
||||
metadata: exercise.metadata,
|
||||
write: () => database
|
||||
.into(database.exercises)
|
||||
.insertOnConflictUpdate(_exerciseCompanion(exercise)),
|
||||
);
|
||||
await database.transaction(() async {
|
||||
await _upsertWithChangeLog(
|
||||
database: database,
|
||||
tableName: 'exercises',
|
||||
entityType: 'Exercise',
|
||||
metadata: exercise.metadata,
|
||||
write: () => database
|
||||
.into(database.exercises)
|
||||
.insertOnConflictUpdate(_exerciseCompanion(exercise)),
|
||||
);
|
||||
await _replaceExerciseImages(database, exercise);
|
||||
});
|
||||
}
|
||||
|
||||
Future<List<String>> _imageMediaIdsForExercise(String exerciseId) async {
|
||||
final rows =
|
||||
await (database.select(database.exerciseImages)
|
||||
..where(
|
||||
(table) =>
|
||||
table.exerciseId.equals(exerciseId) &
|
||||
table.deletedAt.isNull(),
|
||||
)
|
||||
..orderBy([(table) => OrderingTerm.asc(table.position)]))
|
||||
.get();
|
||||
return rows.map((row) => row.mediaAssetId).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@ -736,6 +760,100 @@ Future<void> _upsertWithChangeLog({
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _replaceExerciseImages(
|
||||
db.AppDatabase database,
|
||||
domain.Exercise exercise,
|
||||
) async {
|
||||
final rows = await (database.select(
|
||||
database.exerciseImages,
|
||||
)..where((table) => table.exerciseId.equals(exercise.metadata.id))).get();
|
||||
final activeRowsByMediaId = {
|
||||
for (final row in rows)
|
||||
if (row.deletedAt == null) row.mediaAssetId: row,
|
||||
};
|
||||
final desiredIds = exercise.imageMediaIds.toSet();
|
||||
final removedRows = activeRowsByMediaId.values
|
||||
.where((row) => !desiredIds.contains(row.mediaAssetId))
|
||||
.toList();
|
||||
await _softDeleteExerciseImageRows(
|
||||
database,
|
||||
removedRows,
|
||||
exercise.metadata.updatedAt,
|
||||
);
|
||||
|
||||
for (var index = 0; index < exercise.imageMediaIds.length; index++) {
|
||||
final mediaId = exercise.imageMediaIds[index];
|
||||
final existing = activeRowsByMediaId[mediaId];
|
||||
final metadata = existing == null
|
||||
? domain.EntityMetadata(
|
||||
id: _exerciseImageId(exercise.metadata.id, mediaId),
|
||||
createdAt: exercise.metadata.updatedAt,
|
||||
updatedAt: exercise.metadata.updatedAt,
|
||||
originDeviceId: exercise.metadata.originDeviceId,
|
||||
)
|
||||
: _metadataFromRow(existing).touch(exercise.metadata.updatedAt);
|
||||
await _upsertWithChangeLog(
|
||||
database: database,
|
||||
tableName: 'exercise_images',
|
||||
entityType: 'ExerciseImage',
|
||||
metadata: metadata,
|
||||
write: () => database
|
||||
.into(database.exerciseImages)
|
||||
.insertOnConflictUpdate(
|
||||
db.ExerciseImagesCompanion(
|
||||
id: Value(metadata.id),
|
||||
createdAt: Value(metadata.createdAt.toUtc()),
|
||||
updatedAt: Value(metadata.updatedAt.toUtc()),
|
||||
deletedAt: Value(_utcOrNull(metadata.deletedAt)),
|
||||
schemaVersion: Value(metadata.schemaVersion),
|
||||
syncState: Value(_syncStateToDb(metadata.syncState)),
|
||||
localRevision: Value(metadata.localRevision),
|
||||
originDeviceId: Value(metadata.originDeviceId),
|
||||
futureOwnerProfileId: Value(metadata.futureOwnerProfileId),
|
||||
lastSyncedAt: Value(_utcOrNull(metadata.lastSyncedAt)),
|
||||
remoteRevision: Value(metadata.remoteRevision),
|
||||
exerciseId: Value(exercise.metadata.id),
|
||||
mediaAssetId: Value(mediaId),
|
||||
position: Value(index),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _softDeleteExerciseImageRows(
|
||||
db.AppDatabase database,
|
||||
List<db.ExerciseImage> rows,
|
||||
DateTime deletedAt,
|
||||
) async {
|
||||
for (final row in rows) {
|
||||
final revision = row.localRevision + 1;
|
||||
await (database.update(
|
||||
database.exerciseImages,
|
||||
)..where((table) => table.id.equals(row.id))).write(
|
||||
db.ExerciseImagesCompanion(
|
||||
deletedAt: Value(deletedAt.toUtc()),
|
||||
updatedAt: Value(deletedAt.toUtc()),
|
||||
localRevision: Value(revision),
|
||||
syncState: const Value('deleted'),
|
||||
),
|
||||
);
|
||||
await _writeChangeLog(
|
||||
database: database,
|
||||
entityType: 'ExerciseImage',
|
||||
entityId: row.id,
|
||||
operation: 'softDelete',
|
||||
localRevision: revision,
|
||||
originDeviceId: row.originDeviceId,
|
||||
createdAt: deletedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _exerciseImageId(String exerciseId, String mediaId) {
|
||||
return 'exercise-image:$exerciseId:$mediaId';
|
||||
}
|
||||
|
||||
Future<String> _operationForMutation({
|
||||
required db.AppDatabase database,
|
||||
required String tableName,
|
||||
@ -958,7 +1076,6 @@ db.ExercisesCompanion _exerciseCompanion(domain.Exercise exercise) {
|
||||
remoteRevision: values[10] as Value<String?>,
|
||||
name: Value(exercise.name),
|
||||
description: Value(exercise.description),
|
||||
imageMediaId: Value(exercise.imageMediaId),
|
||||
videoMediaId: Value(exercise.videoMediaId),
|
||||
hasTimeMeasure: Value(exercise.hasTimeMeasure),
|
||||
hasRepsMeasure: Value(exercise.hasRepsMeasure),
|
||||
@ -974,12 +1091,12 @@ db.ExercisesCompanion _exerciseCompanion(domain.Exercise exercise) {
|
||||
);
|
||||
}
|
||||
|
||||
domain.Exercise _exerciseFromRow(db.Exercise row) {
|
||||
domain.Exercise _exerciseFromRow(db.Exercise row, List<String> imageMediaIds) {
|
||||
return domain.Exercise(
|
||||
metadata: _metadataFromRow(row),
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
imageMediaId: row.imageMediaId,
|
||||
imageMediaIds: imageMediaIds,
|
||||
videoMediaId: row.videoMediaId,
|
||||
hasTimeMeasure: row.hasTimeMeasure,
|
||||
hasRepsMeasure: row.hasRepsMeasure,
|
||||
|
||||
Reference in New Issue
Block a user