From 8bda4758f2e9d46febe2547f0b505a8a709f9f89 Mon Sep 17 00:00:00 2001 From: Blomios Date: Sat, 18 Jul 2026 14:02:40 +0200 Subject: [PATCH 1/2] feat(exercice): galerie de 5 images par exercice (ticket #35) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit É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 --- lib/application/use_cases.dart | 97 +- lib/domain/entities.dart | 37 +- lib/infrastructure/local/app_database.dart | 26 +- lib/infrastructure/local/app_database.g.dart | 1840 +++++++++++++++-- .../local/drift_repositories.dart | 145 +- lib/infrastructure/local/tables.dart | 20 +- lib/presentation/exercise_library_screen.dart | 220 +- test/application/use_cases_test.dart | 69 + .../exercise_library_screen_test.dart | 95 +- 9 files changed, 2250 insertions(+), 299 deletions(-) diff --git a/lib/application/use_cases.dart b/lib/application/use_cases.dart index a780ec0..e6742f0 100644 --- a/lib/application/use_cases.dart +++ b/lib/application/use_cases.dart @@ -24,6 +24,7 @@ final class ExerciseUseCases { required String name, String? description, String? imageMediaId, + List imageMediaIds = const [], String? videoMediaId, required bool hasTimeMeasure, required bool hasRepsMeasure, @@ -51,7 +52,7 @@ final class ExerciseUseCases { metadata: _newMetadata(ids, originDeviceId, now), name: name, description: description, - imageMediaId: imageMediaId, + imageMediaIds: _resolveExerciseImageIds(imageMediaId, imageMediaIds), videoMediaId: videoMediaId, hasTimeMeasure: hasTimeMeasure, hasRepsMeasure: hasRepsMeasure, @@ -81,6 +82,7 @@ final class ExerciseUseCases { required String name, String? description, String? imageMediaId, + Object? imageMediaIds = _useCaseUnchanged, String? videoMediaId, required bool hasTimeMeasure, required bool hasRepsMeasure, @@ -125,7 +127,9 @@ final class ExerciseUseCases { metadata: exercise.metadata.touch(clock.now()), name: name, description: description, - imageMediaId: imageMediaId, + imageMediaIds: imageMediaIds == _useCaseUnchanged + ? _resolveExerciseImageIds(imageMediaId, exercise.imageMediaIds) + : imageMediaIds as List, videoMediaId: videoMediaId, hasTimeMeasure: hasTimeMeasure, hasRepsMeasure: hasRepsMeasure, @@ -151,7 +155,62 @@ final class ExerciseUseCases { throw const DomainException('Exercise not found.'); } final updated = exercise.copyWith( - imageMediaId: mediaAssetId, + imageMediaIds: _appendExerciseImage(exercise.imageMediaIds, mediaAssetId), + metadata: exercise.metadata.touch(clock.now()), + ); + await repository.save(updated); + return updated; + } + + Future addImage({ + 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( + imageMediaIds: _appendExerciseImage(exercise.imageMediaIds, mediaAssetId), + metadata: exercise.metadata.touch(clock.now()), + ); + await repository.save(updated); + return updated; + } + + Future removeImage({ + 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( + imageMediaIds: exercise.imageMediaIds + .where((imageId) => imageId != mediaAssetId) + .toList(), + metadata: exercise.metadata.touch(clock.now()), + ); + await repository.save(updated); + return updated; + } + + Future reorderImages({ + required String exerciseId, + required List orderedMediaIds, + }) async { + final exercise = await repository.findById(exerciseId); + if (exercise == null) { + throw const DomainException('Exercise not found.'); + } + if (!_sameImageSet(exercise.imageMediaIds, orderedMediaIds)) { + throw const DomainException( + 'Reordered exercise images must match existing images.', + ); + } + final updated = exercise.copyWith( + imageMediaIds: orderedMediaIds, metadata: exercise.metadata.touch(clock.now()), ); await repository.save(updated); @@ -275,7 +334,10 @@ final class MediaUseCases { } final updated = switch (media.kind) { MediaKind.image => exercise.copyWith( - imageMediaId: media.metadata.id, + imageMediaIds: _appendExerciseImage( + exercise.imageMediaIds, + media.metadata.id, + ), metadata: exercise.metadata.touch(clock.now()), ), MediaKind.video => exercise.copyWith( @@ -1479,6 +1541,33 @@ List _repositionWorkoutTemplatePrograms( ]; } +List _resolveExerciseImageIds( + String? imageMediaId, + List imageMediaIds, +) { + if (imageMediaId != null) { + return [imageMediaId]; + } + return imageMediaIds; +} + +List _appendExerciseImage(List currentIds, String mediaId) { + if (currentIds.contains(mediaId)) { + return currentIds; + } + if (currentIds.length >= 5) { + throw const DomainException('An exercise cannot have more than 5 images.'); + } + return [...currentIds, mediaId]; +} + +bool _sameImageSet(List left, List right) { + if (left.length != right.length) { + return false; + } + return left.toSet().containsAll(right) && right.toSet().containsAll(left); +} + void _validateExerciseDefaultTargets({ required bool hasTimeMeasure, required bool hasRepsMeasure, diff --git a/lib/domain/entities.dart b/lib/domain/entities.dart index f2031f1..07de34a 100644 --- a/lib/domain/entities.dart +++ b/lib/domain/entities.dart @@ -124,7 +124,8 @@ final class Exercise { required this.metadata, required String name, this.description, - this.imageMediaId, + String? imageMediaId, + List imageMediaIds = const [], this.videoMediaId, required this.hasTimeMeasure, required this.hasRepsMeasure, @@ -137,7 +138,12 @@ final class Exercise { this.defaultTargetScore, this.defaultTargetScoreTimeMs, this.archivedAt, - }) : name = _nonBlank(name, 'Exercise name') { + }) : name = _nonBlank(name, 'Exercise name'), + imageMediaIds = _validatedImageMediaIds( + imageMediaIds.isEmpty && imageMediaId != null + ? [imageMediaId] + : imageMediaIds, + ) { _requireAtLeastOneMeasure( hasTime: hasTimeMeasure, hasReps: hasRepsMeasure, @@ -167,7 +173,7 @@ final class Exercise { final EntityMetadata metadata; final String name; final String? description; - final String? imageMediaId; + final List imageMediaIds; final String? videoMediaId; final bool hasTimeMeasure; final bool hasRepsMeasure; @@ -187,6 +193,9 @@ final class Exercise { if (hasScoreMeasure) WorkoutMeasure.score, }; + String? get imageMediaId => + imageMediaIds.isEmpty ? null : imageMediaIds.first; + Exercise archive(DateTime now) { return copyWith(archivedAt: now, metadata: metadata.touch(now)); } @@ -196,6 +205,7 @@ final class Exercise { String? name, Object? description = _unchanged, Object? imageMediaId = _unchanged, + Object? imageMediaIds = _unchanged, Object? videoMediaId = _unchanged, bool? hasTimeMeasure, bool? hasRepsMeasure, @@ -215,9 +225,11 @@ final class Exercise { description: description == _unchanged ? this.description : description as String?, - imageMediaId: imageMediaId == _unchanged - ? this.imageMediaId - : imageMediaId as String?, + imageMediaIds: imageMediaIds == _unchanged + ? imageMediaId == _unchanged + ? this.imageMediaIds + : [if (imageMediaId != null) imageMediaId as String] + : imageMediaIds as List, videoMediaId: videoMediaId == _unchanged ? this.videoMediaId : videoMediaId as String?, @@ -921,6 +933,19 @@ String _nonBlank(String? value, String label) { return trimmed; } +List _validatedImageMediaIds(List ids) { + if (ids.length > 5) { + throw const DomainException('An exercise cannot have more than 5 images.'); + } + final normalized = ids + .map((id) => _nonBlank(id, 'Image media id')) + .toList(growable: false); + if (normalized.toSet().length != normalized.length) { + throw const DomainException('Exercise images must be unique.'); + } + return normalized; +} + void _requireAtLeastOneMeasure({ required bool hasTime, required bool hasReps, diff --git a/lib/infrastructure/local/app_database.dart b/lib/infrastructure/local/app_database.dart index e6a9b42..1899ec1 100644 --- a/lib/infrastructure/local/app_database.dart +++ b/lib/infrastructure/local/app_database.dart @@ -13,6 +13,7 @@ part 'app_database.g.dart'; ActiveWorkoutSessions, ChangeLogEntries, Exercises, + ExerciseImages, MediaAssets, ProgramExercises, Programs, @@ -36,7 +37,7 @@ final class AppDatabase extends _$AppDatabase { } @override - int get schemaVersion => 4; + int get schemaVersion => 5; @override MigrationStrategy get migration { @@ -63,6 +64,9 @@ final class AppDatabase extends _$AppDatabase { if (from < 4) { await _migrateToSchema4(); } + if (from < 5) { + await _migrateToSchema5(migrator); + } await _createIndexes(); }, beforeOpen: (details) async { @@ -95,6 +99,10 @@ final class AppDatabase extends _$AppDatabase { 'CREATE INDEX IF NOT EXISTS idx_program_exercises_program_id ' 'ON program_exercises (program_id)', ); + await customStatement( + 'CREATE INDEX IF NOT EXISTS idx_exercise_images_exercise_id ' + 'ON exercise_images (exercise_id)', + ); await customStatement( 'CREATE INDEX IF NOT EXISTS idx_workout_template_programs_template_id ' 'ON workout_template_programs (workout_template_id)', @@ -147,6 +155,7 @@ const _syncableTableNames = [ 'active_set_results', 'active_workout_sessions', 'exercises', + 'exercise_images', 'media_assets', 'program_exercises', 'programs', @@ -226,4 +235,19 @@ extension on AppDatabase { 'default_target_score_time_ms > 0)', ); } + + Future _migrateToSchema5(Migrator migrator) async { + await migrator.createTable(exerciseImages); + await customStatement( + 'INSERT OR IGNORE INTO exercise_images (id, created_at, updated_at, ' + 'deleted_at, schema_version, sync_state, local_revision, ' + 'origin_device_id, future_owner_profile_id, last_synced_at, ' + 'remote_revision, exercise_id, media_asset_id, position) ' + "SELECT 'exercise-image:' || id || ':0', created_at, updated_at, " + 'deleted_at, schema_version, sync_state, local_revision, ' + 'origin_device_id, future_owner_profile_id, last_synced_at, ' + 'remote_revision, id, image_media_id, 0 FROM exercises ' + 'WHERE image_media_id IS NOT NULL', + ); + } } diff --git a/lib/infrastructure/local/app_database.g.dart b/lib/infrastructure/local/app_database.g.dart index 8c32fb3..75ad807 100644 --- a/lib/infrastructure/local/app_database.g.dart +++ b/lib/infrastructure/local/app_database.g.dart @@ -7959,20 +7959,6 @@ class $ExercisesTable extends Exercises type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _imageMediaIdMeta = const VerificationMeta( - 'imageMediaId', - ); - @override - late final GeneratedColumn imageMediaId = GeneratedColumn( - 'image_media_id', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES media_assets (id)', - ), - ); static const VerificationMeta _videoMediaIdMeta = const VerificationMeta( 'videoMediaId', ); @@ -8133,7 +8119,6 @@ class $ExercisesTable extends Exercises remoteRevision, name, description, - imageMediaId, videoMediaId, hasTimeMeasure, hasRepsMeasure, @@ -8269,15 +8254,6 @@ class $ExercisesTable extends Exercises ), ); } - if (data.containsKey('image_media_id')) { - context.handle( - _imageMediaIdMeta, - imageMediaId.isAcceptableOrUnknown( - data['image_media_id']!, - _imageMediaIdMeta, - ), - ); - } if (data.containsKey('video_media_id')) { context.handle( _videoMediaIdMeta, @@ -8444,10 +8420,6 @@ class $ExercisesTable extends Exercises DriftSqlType.string, data['${effectivePrefix}description'], ), - imageMediaId: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}image_media_id'], - ), videoMediaId: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}video_media_id'], @@ -8519,7 +8491,6 @@ class Exercise extends DataClass implements Insertable { final String? remoteRevision; final String name; final String? description; - final String? imageMediaId; final String? videoMediaId; final bool hasTimeMeasure; final bool hasRepsMeasure; @@ -8546,7 +8517,6 @@ class Exercise extends DataClass implements Insertable { this.remoteRevision, required this.name, this.description, - this.imageMediaId, this.videoMediaId, required this.hasTimeMeasure, required this.hasRepsMeasure, @@ -8586,9 +8556,6 @@ class Exercise extends DataClass implements Insertable { if (!nullToAbsent || description != null) { map['description'] = Variable(description); } - if (!nullToAbsent || imageMediaId != null) { - map['image_media_id'] = Variable(imageMediaId); - } if (!nullToAbsent || videoMediaId != null) { map['video_media_id'] = Variable(videoMediaId); } @@ -8649,9 +8616,6 @@ class Exercise extends DataClass implements Insertable { description: description == null && nullToAbsent ? const Value.absent() : Value(description), - imageMediaId: imageMediaId == null && nullToAbsent - ? const Value.absent() - : Value(imageMediaId), videoMediaId: videoMediaId == null && nullToAbsent ? const Value.absent() : Value(videoMediaId), @@ -8704,7 +8668,6 @@ class Exercise extends DataClass implements Insertable { remoteRevision: serializer.fromJson(json['remoteRevision']), name: serializer.fromJson(json['name']), description: serializer.fromJson(json['description']), - imageMediaId: serializer.fromJson(json['imageMediaId']), videoMediaId: serializer.fromJson(json['videoMediaId']), hasTimeMeasure: serializer.fromJson(json['hasTimeMeasure']), hasRepsMeasure: serializer.fromJson(json['hasRepsMeasure']), @@ -8742,7 +8705,6 @@ class Exercise extends DataClass implements Insertable { 'remoteRevision': serializer.toJson(remoteRevision), 'name': serializer.toJson(name), 'description': serializer.toJson(description), - 'imageMediaId': serializer.toJson(imageMediaId), 'videoMediaId': serializer.toJson(videoMediaId), 'hasTimeMeasure': serializer.toJson(hasTimeMeasure), 'hasRepsMeasure': serializer.toJson(hasRepsMeasure), @@ -8776,7 +8738,6 @@ class Exercise extends DataClass implements Insertable { Value remoteRevision = const Value.absent(), String? name, Value description = const Value.absent(), - Value imageMediaId = const Value.absent(), Value videoMediaId = const Value.absent(), bool? hasTimeMeasure, bool? hasRepsMeasure, @@ -8807,7 +8768,6 @@ class Exercise extends DataClass implements Insertable { : this.remoteRevision, name: name ?? this.name, description: description.present ? description.value : this.description, - imageMediaId: imageMediaId.present ? imageMediaId.value : this.imageMediaId, videoMediaId: videoMediaId.present ? videoMediaId.value : this.videoMediaId, hasTimeMeasure: hasTimeMeasure ?? this.hasTimeMeasure, hasRepsMeasure: hasRepsMeasure ?? this.hasRepsMeasure, @@ -8858,9 +8818,6 @@ class Exercise extends DataClass implements Insertable { description: data.description.present ? data.description.value : this.description, - imageMediaId: data.imageMediaId.present - ? data.imageMediaId.value - : this.imageMediaId, videoMediaId: data.videoMediaId.present ? data.videoMediaId.value : this.videoMediaId, @@ -8914,7 +8871,6 @@ class Exercise extends DataClass implements Insertable { ..write('remoteRevision: $remoteRevision, ') ..write('name: $name, ') ..write('description: $description, ') - ..write('imageMediaId: $imageMediaId, ') ..write('videoMediaId: $videoMediaId, ') ..write('hasTimeMeasure: $hasTimeMeasure, ') ..write('hasRepsMeasure: $hasRepsMeasure, ') @@ -8946,7 +8902,6 @@ class Exercise extends DataClass implements Insertable { remoteRevision, name, description, - imageMediaId, videoMediaId, hasTimeMeasure, hasRepsMeasure, @@ -8977,7 +8932,6 @@ class Exercise extends DataClass implements Insertable { other.remoteRevision == this.remoteRevision && other.name == this.name && other.description == this.description && - other.imageMediaId == this.imageMediaId && other.videoMediaId == this.videoMediaId && other.hasTimeMeasure == this.hasTimeMeasure && other.hasRepsMeasure == this.hasRepsMeasure && @@ -9006,7 +8960,6 @@ class ExercisesCompanion extends UpdateCompanion { final Value remoteRevision; final Value name; final Value description; - final Value imageMediaId; final Value videoMediaId; final Value hasTimeMeasure; final Value hasRepsMeasure; @@ -9034,7 +8987,6 @@ class ExercisesCompanion extends UpdateCompanion { this.remoteRevision = const Value.absent(), this.name = const Value.absent(), this.description = const Value.absent(), - this.imageMediaId = const Value.absent(), this.videoMediaId = const Value.absent(), this.hasTimeMeasure = const Value.absent(), this.hasRepsMeasure = const Value.absent(), @@ -9063,7 +9015,6 @@ class ExercisesCompanion extends UpdateCompanion { this.remoteRevision = const Value.absent(), required String name, this.description = const Value.absent(), - this.imageMediaId = const Value.absent(), this.videoMediaId = const Value.absent(), required bool hasTimeMeasure, required bool hasRepsMeasure, @@ -9101,7 +9052,6 @@ class ExercisesCompanion extends UpdateCompanion { Expression? remoteRevision, Expression? name, Expression? description, - Expression? imageMediaId, Expression? videoMediaId, Expression? hasTimeMeasure, Expression? hasRepsMeasure, @@ -9131,7 +9081,6 @@ class ExercisesCompanion extends UpdateCompanion { if (remoteRevision != null) 'remote_revision': remoteRevision, if (name != null) 'name': name, if (description != null) 'description': description, - if (imageMediaId != null) 'image_media_id': imageMediaId, if (videoMediaId != null) 'video_media_id': videoMediaId, if (hasTimeMeasure != null) 'has_time_measure': hasTimeMeasure, if (hasRepsMeasure != null) 'has_reps_measure': hasRepsMeasure, @@ -9165,7 +9114,6 @@ class ExercisesCompanion extends UpdateCompanion { Value? remoteRevision, Value? name, Value? description, - Value? imageMediaId, Value? videoMediaId, Value? hasTimeMeasure, Value? hasRepsMeasure, @@ -9194,7 +9142,6 @@ class ExercisesCompanion extends UpdateCompanion { remoteRevision: remoteRevision ?? this.remoteRevision, name: name ?? this.name, description: description ?? this.description, - imageMediaId: imageMediaId ?? this.imageMediaId, videoMediaId: videoMediaId ?? this.videoMediaId, hasTimeMeasure: hasTimeMeasure ?? this.hasTimeMeasure, hasRepsMeasure: hasRepsMeasure ?? this.hasRepsMeasure, @@ -9257,9 +9204,6 @@ class ExercisesCompanion extends UpdateCompanion { if (description.present) { map['description'] = Variable(description.value); } - if (imageMediaId.present) { - map['image_media_id'] = Variable(imageMediaId.value); - } if (videoMediaId.present) { map['video_media_id'] = Variable(videoMediaId.value); } @@ -9322,7 +9266,6 @@ class ExercisesCompanion extends UpdateCompanion { ..write('remoteRevision: $remoteRevision, ') ..write('name: $name, ') ..write('description: $description, ') - ..write('imageMediaId: $imageMediaId, ') ..write('videoMediaId: $videoMediaId, ') ..write('hasTimeMeasure: $hasTimeMeasure, ') ..write('hasRepsMeasure: $hasRepsMeasure, ') @@ -9341,6 +9284,864 @@ class ExercisesCompanion extends UpdateCompanion { } } +class $ExerciseImagesTable extends ExerciseImages + with TableInfo<$ExerciseImagesTable, ExerciseImage> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ExerciseImagesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + static const VerificationMeta _deletedAtMeta = const VerificationMeta( + 'deletedAt', + ); + @override + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + static const VerificationMeta _schemaVersionMeta = const VerificationMeta( + 'schemaVersion', + ); + @override + late final GeneratedColumn schemaVersion = GeneratedColumn( + 'schema_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(1), + ); + static const VerificationMeta _syncStateMeta = const VerificationMeta( + 'syncState', + ); + @override + late final GeneratedColumn syncState = GeneratedColumn( + 'sync_state', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL CHECK (sync_state IN (\'localOnly\', \'dirty\', \'synced\', \'deleted\'))', + ); + static const VerificationMeta _localRevisionMeta = const VerificationMeta( + 'localRevision', + ); + @override + late final GeneratedColumn localRevision = GeneratedColumn( + 'local_revision', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL CHECK (local_revision >= 0)', + ); + static const VerificationMeta _originDeviceIdMeta = const VerificationMeta( + 'originDeviceId', + ); + @override + late final GeneratedColumn originDeviceId = GeneratedColumn( + 'origin_device_id', + aliasedName, + false, + additionalChecks: GeneratedColumn.checkTextLength(minTextLength: 1), + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _futureOwnerProfileIdMeta = + const VerificationMeta('futureOwnerProfileId'); + @override + late final GeneratedColumn futureOwnerProfileId = + GeneratedColumn( + 'future_owner_profile_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _lastSyncedAtMeta = const VerificationMeta( + 'lastSyncedAt', + ); + @override + late final GeneratedColumn lastSyncedAt = GeneratedColumn( + 'last_synced_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + static const VerificationMeta _remoteRevisionMeta = const VerificationMeta( + 'remoteRevision', + ); + @override + late final GeneratedColumn remoteRevision = GeneratedColumn( + 'remote_revision', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _exerciseIdMeta = const VerificationMeta( + 'exerciseId', + ); + @override + late final GeneratedColumn exerciseId = GeneratedColumn( + 'exercise_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES exercises (id)', + ), + ); + static const VerificationMeta _mediaAssetIdMeta = const VerificationMeta( + 'mediaAssetId', + ); + @override + late final GeneratedColumn mediaAssetId = GeneratedColumn( + 'media_asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES media_assets (id)', + ), + ); + static const VerificationMeta _positionMeta = const VerificationMeta( + 'position', + ); + @override + late final GeneratedColumn position = GeneratedColumn( + 'position', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + deletedAt, + schemaVersion, + syncState, + localRevision, + originDeviceId, + futureOwnerProfileId, + lastSyncedAt, + remoteRevision, + exerciseId, + mediaAssetId, + position, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'exercise_images'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } else if (isInserting) { + context.missing(_createdAtMeta); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } else if (isInserting) { + context.missing(_updatedAtMeta); + } + if (data.containsKey('deleted_at')) { + context.handle( + _deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta), + ); + } + if (data.containsKey('schema_version')) { + context.handle( + _schemaVersionMeta, + schemaVersion.isAcceptableOrUnknown( + data['schema_version']!, + _schemaVersionMeta, + ), + ); + } + if (data.containsKey('sync_state')) { + context.handle( + _syncStateMeta, + syncState.isAcceptableOrUnknown(data['sync_state']!, _syncStateMeta), + ); + } else if (isInserting) { + context.missing(_syncStateMeta); + } + if (data.containsKey('local_revision')) { + context.handle( + _localRevisionMeta, + localRevision.isAcceptableOrUnknown( + data['local_revision']!, + _localRevisionMeta, + ), + ); + } else if (isInserting) { + context.missing(_localRevisionMeta); + } + if (data.containsKey('origin_device_id')) { + context.handle( + _originDeviceIdMeta, + originDeviceId.isAcceptableOrUnknown( + data['origin_device_id']!, + _originDeviceIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_originDeviceIdMeta); + } + if (data.containsKey('future_owner_profile_id')) { + context.handle( + _futureOwnerProfileIdMeta, + futureOwnerProfileId.isAcceptableOrUnknown( + data['future_owner_profile_id']!, + _futureOwnerProfileIdMeta, + ), + ); + } + if (data.containsKey('last_synced_at')) { + context.handle( + _lastSyncedAtMeta, + lastSyncedAt.isAcceptableOrUnknown( + data['last_synced_at']!, + _lastSyncedAtMeta, + ), + ); + } + if (data.containsKey('remote_revision')) { + context.handle( + _remoteRevisionMeta, + remoteRevision.isAcceptableOrUnknown( + data['remote_revision']!, + _remoteRevisionMeta, + ), + ); + } + if (data.containsKey('exercise_id')) { + context.handle( + _exerciseIdMeta, + exerciseId.isAcceptableOrUnknown(data['exercise_id']!, _exerciseIdMeta), + ); + } else if (isInserting) { + context.missing(_exerciseIdMeta); + } + if (data.containsKey('media_asset_id')) { + context.handle( + _mediaAssetIdMeta, + mediaAssetId.isAcceptableOrUnknown( + data['media_asset_id']!, + _mediaAssetIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_mediaAssetIdMeta); + } + if (data.containsKey('position')) { + context.handle( + _positionMeta, + position.isAcceptableOrUnknown(data['position']!, _positionMeta), + ); + } else if (isInserting) { + context.missing(_positionMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + ExerciseImage map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ExerciseImage( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + schemaVersion: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}schema_version'], + )!, + syncState: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_state'], + )!, + localRevision: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}local_revision'], + )!, + originDeviceId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}origin_device_id'], + )!, + futureOwnerProfileId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}future_owner_profile_id'], + ), + lastSyncedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}last_synced_at'], + ), + remoteRevision: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}remote_revision'], + ), + exerciseId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}exercise_id'], + )!, + mediaAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}media_asset_id'], + )!, + position: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}position'], + )!, + ); + } + + @override + $ExerciseImagesTable createAlias(String alias) { + return $ExerciseImagesTable(attachedDatabase, alias); + } +} + +class ExerciseImage extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final int schemaVersion; + final String syncState; + final int localRevision; + final String originDeviceId; + final String? futureOwnerProfileId; + final DateTime? lastSyncedAt; + final String? remoteRevision; + final String exerciseId; + final String mediaAssetId; + final int position; + const ExerciseImage({ + required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.schemaVersion, + required this.syncState, + required this.localRevision, + required this.originDeviceId, + this.futureOwnerProfileId, + this.lastSyncedAt, + this.remoteRevision, + required this.exerciseId, + required this.mediaAssetId, + required this.position, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + map['schema_version'] = Variable(schemaVersion); + map['sync_state'] = Variable(syncState); + map['local_revision'] = Variable(localRevision); + map['origin_device_id'] = Variable(originDeviceId); + if (!nullToAbsent || futureOwnerProfileId != null) { + map['future_owner_profile_id'] = Variable(futureOwnerProfileId); + } + if (!nullToAbsent || lastSyncedAt != null) { + map['last_synced_at'] = Variable(lastSyncedAt); + } + if (!nullToAbsent || remoteRevision != null) { + map['remote_revision'] = Variable(remoteRevision); + } + map['exercise_id'] = Variable(exerciseId); + map['media_asset_id'] = Variable(mediaAssetId); + map['position'] = Variable(position); + return map; + } + + ExerciseImagesCompanion toCompanion(bool nullToAbsent) { + return ExerciseImagesCompanion( + id: Value(id), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + deletedAt: deletedAt == null && nullToAbsent + ? const Value.absent() + : Value(deletedAt), + schemaVersion: Value(schemaVersion), + syncState: Value(syncState), + localRevision: Value(localRevision), + originDeviceId: Value(originDeviceId), + futureOwnerProfileId: futureOwnerProfileId == null && nullToAbsent + ? const Value.absent() + : Value(futureOwnerProfileId), + lastSyncedAt: lastSyncedAt == null && nullToAbsent + ? const Value.absent() + : Value(lastSyncedAt), + remoteRevision: remoteRevision == null && nullToAbsent + ? const Value.absent() + : Value(remoteRevision), + exerciseId: Value(exerciseId), + mediaAssetId: Value(mediaAssetId), + position: Value(position), + ); + } + + factory ExerciseImage.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ExerciseImage( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + schemaVersion: serializer.fromJson(json['schemaVersion']), + syncState: serializer.fromJson(json['syncState']), + localRevision: serializer.fromJson(json['localRevision']), + originDeviceId: serializer.fromJson(json['originDeviceId']), + futureOwnerProfileId: serializer.fromJson( + json['futureOwnerProfileId'], + ), + lastSyncedAt: serializer.fromJson(json['lastSyncedAt']), + remoteRevision: serializer.fromJson(json['remoteRevision']), + exerciseId: serializer.fromJson(json['exerciseId']), + mediaAssetId: serializer.fromJson(json['mediaAssetId']), + position: serializer.fromJson(json['position']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'schemaVersion': serializer.toJson(schemaVersion), + 'syncState': serializer.toJson(syncState), + 'localRevision': serializer.toJson(localRevision), + 'originDeviceId': serializer.toJson(originDeviceId), + 'futureOwnerProfileId': serializer.toJson(futureOwnerProfileId), + 'lastSyncedAt': serializer.toJson(lastSyncedAt), + 'remoteRevision': serializer.toJson(remoteRevision), + 'exerciseId': serializer.toJson(exerciseId), + 'mediaAssetId': serializer.toJson(mediaAssetId), + 'position': serializer.toJson(position), + }; + } + + ExerciseImage copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + int? schemaVersion, + String? syncState, + int? localRevision, + String? originDeviceId, + Value futureOwnerProfileId = const Value.absent(), + Value lastSyncedAt = const Value.absent(), + Value remoteRevision = const Value.absent(), + String? exerciseId, + String? mediaAssetId, + int? position, + }) => ExerciseImage( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + schemaVersion: schemaVersion ?? this.schemaVersion, + syncState: syncState ?? this.syncState, + localRevision: localRevision ?? this.localRevision, + originDeviceId: originDeviceId ?? this.originDeviceId, + futureOwnerProfileId: futureOwnerProfileId.present + ? futureOwnerProfileId.value + : this.futureOwnerProfileId, + lastSyncedAt: lastSyncedAt.present ? lastSyncedAt.value : this.lastSyncedAt, + remoteRevision: remoteRevision.present + ? remoteRevision.value + : this.remoteRevision, + exerciseId: exerciseId ?? this.exerciseId, + mediaAssetId: mediaAssetId ?? this.mediaAssetId, + position: position ?? this.position, + ); + ExerciseImage copyWithCompanion(ExerciseImagesCompanion data) { + return ExerciseImage( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + schemaVersion: data.schemaVersion.present + ? data.schemaVersion.value + : this.schemaVersion, + syncState: data.syncState.present ? data.syncState.value : this.syncState, + localRevision: data.localRevision.present + ? data.localRevision.value + : this.localRevision, + originDeviceId: data.originDeviceId.present + ? data.originDeviceId.value + : this.originDeviceId, + futureOwnerProfileId: data.futureOwnerProfileId.present + ? data.futureOwnerProfileId.value + : this.futureOwnerProfileId, + lastSyncedAt: data.lastSyncedAt.present + ? data.lastSyncedAt.value + : this.lastSyncedAt, + remoteRevision: data.remoteRevision.present + ? data.remoteRevision.value + : this.remoteRevision, + exerciseId: data.exerciseId.present + ? data.exerciseId.value + : this.exerciseId, + mediaAssetId: data.mediaAssetId.present + ? data.mediaAssetId.value + : this.mediaAssetId, + position: data.position.present ? data.position.value : this.position, + ); + } + + @override + String toString() { + return (StringBuffer('ExerciseImage(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('schemaVersion: $schemaVersion, ') + ..write('syncState: $syncState, ') + ..write('localRevision: $localRevision, ') + ..write('originDeviceId: $originDeviceId, ') + ..write('futureOwnerProfileId: $futureOwnerProfileId, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('remoteRevision: $remoteRevision, ') + ..write('exerciseId: $exerciseId, ') + ..write('mediaAssetId: $mediaAssetId, ') + ..write('position: $position') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + deletedAt, + schemaVersion, + syncState, + localRevision, + originDeviceId, + futureOwnerProfileId, + lastSyncedAt, + remoteRevision, + exerciseId, + mediaAssetId, + position, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ExerciseImage && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.schemaVersion == this.schemaVersion && + other.syncState == this.syncState && + other.localRevision == this.localRevision && + other.originDeviceId == this.originDeviceId && + other.futureOwnerProfileId == this.futureOwnerProfileId && + other.lastSyncedAt == this.lastSyncedAt && + other.remoteRevision == this.remoteRevision && + other.exerciseId == this.exerciseId && + other.mediaAssetId == this.mediaAssetId && + other.position == this.position); +} + +class ExerciseImagesCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value schemaVersion; + final Value syncState; + final Value localRevision; + final Value originDeviceId; + final Value futureOwnerProfileId; + final Value lastSyncedAt; + final Value remoteRevision; + final Value exerciseId; + final Value mediaAssetId; + final Value position; + final Value rowid; + const ExerciseImagesCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.schemaVersion = const Value.absent(), + this.syncState = const Value.absent(), + this.localRevision = const Value.absent(), + this.originDeviceId = const Value.absent(), + this.futureOwnerProfileId = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + this.remoteRevision = const Value.absent(), + this.exerciseId = const Value.absent(), + this.mediaAssetId = const Value.absent(), + this.position = const Value.absent(), + this.rowid = const Value.absent(), + }); + ExerciseImagesCompanion.insert({ + required String id, + required DateTime createdAt, + required DateTime updatedAt, + this.deletedAt = const Value.absent(), + this.schemaVersion = const Value.absent(), + required String syncState, + required int localRevision, + required String originDeviceId, + this.futureOwnerProfileId = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + this.remoteRevision = const Value.absent(), + required String exerciseId, + required String mediaAssetId, + required int position, + this.rowid = const Value.absent(), + }) : id = Value(id), + createdAt = Value(createdAt), + updatedAt = Value(updatedAt), + syncState = Value(syncState), + localRevision = Value(localRevision), + originDeviceId = Value(originDeviceId), + exerciseId = Value(exerciseId), + mediaAssetId = Value(mediaAssetId), + position = Value(position); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? schemaVersion, + Expression? syncState, + Expression? localRevision, + Expression? originDeviceId, + Expression? futureOwnerProfileId, + Expression? lastSyncedAt, + Expression? remoteRevision, + Expression? exerciseId, + Expression? mediaAssetId, + Expression? position, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (schemaVersion != null) 'schema_version': schemaVersion, + if (syncState != null) 'sync_state': syncState, + if (localRevision != null) 'local_revision': localRevision, + if (originDeviceId != null) 'origin_device_id': originDeviceId, + if (futureOwnerProfileId != null) + 'future_owner_profile_id': futureOwnerProfileId, + if (lastSyncedAt != null) 'last_synced_at': lastSyncedAt, + if (remoteRevision != null) 'remote_revision': remoteRevision, + if (exerciseId != null) 'exercise_id': exerciseId, + if (mediaAssetId != null) 'media_asset_id': mediaAssetId, + if (position != null) 'position': position, + if (rowid != null) 'rowid': rowid, + }); + } + + ExerciseImagesCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? schemaVersion, + Value? syncState, + Value? localRevision, + Value? originDeviceId, + Value? futureOwnerProfileId, + Value? lastSyncedAt, + Value? remoteRevision, + Value? exerciseId, + Value? mediaAssetId, + Value? position, + Value? rowid, + }) { + return ExerciseImagesCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + schemaVersion: schemaVersion ?? this.schemaVersion, + syncState: syncState ?? this.syncState, + localRevision: localRevision ?? this.localRevision, + originDeviceId: originDeviceId ?? this.originDeviceId, + futureOwnerProfileId: futureOwnerProfileId ?? this.futureOwnerProfileId, + lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt, + remoteRevision: remoteRevision ?? this.remoteRevision, + exerciseId: exerciseId ?? this.exerciseId, + mediaAssetId: mediaAssetId ?? this.mediaAssetId, + position: position ?? this.position, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (schemaVersion.present) { + map['schema_version'] = Variable(schemaVersion.value); + } + if (syncState.present) { + map['sync_state'] = Variable(syncState.value); + } + if (localRevision.present) { + map['local_revision'] = Variable(localRevision.value); + } + if (originDeviceId.present) { + map['origin_device_id'] = Variable(originDeviceId.value); + } + if (futureOwnerProfileId.present) { + map['future_owner_profile_id'] = Variable( + futureOwnerProfileId.value, + ); + } + if (lastSyncedAt.present) { + map['last_synced_at'] = Variable(lastSyncedAt.value); + } + if (remoteRevision.present) { + map['remote_revision'] = Variable(remoteRevision.value); + } + if (exerciseId.present) { + map['exercise_id'] = Variable(exerciseId.value); + } + if (mediaAssetId.present) { + map['media_asset_id'] = Variable(mediaAssetId.value); + } + if (position.present) { + map['position'] = Variable(position.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ExerciseImagesCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('schemaVersion: $schemaVersion, ') + ..write('syncState: $syncState, ') + ..write('localRevision: $localRevision, ') + ..write('originDeviceId: $originDeviceId, ') + ..write('futureOwnerProfileId: $futureOwnerProfileId, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('remoteRevision: $remoteRevision, ') + ..write('exerciseId: $exerciseId, ') + ..write('mediaAssetId: $mediaAssetId, ') + ..write('position: $position, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + class $ProgramsTable extends Programs with TableInfo<$ProgramsTable, Program> { @override final GeneratedDatabase attachedDatabase; @@ -17695,6 +18496,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { ); late final $MediaAssetsTable mediaAssets = $MediaAssetsTable(this); late final $ExercisesTable exercises = $ExercisesTable(this); + late final $ExerciseImagesTable exerciseImages = $ExerciseImagesTable(this); late final $ProgramsTable programs = $ProgramsTable(this); late final $ProgramExercisesTable programExercises = $ProgramExercisesTable( this, @@ -17723,6 +18525,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { changeLogEntries, mediaAssets, exercises, + exerciseImages, programs, programExercises, workoutHistories, @@ -21983,27 +22786,6 @@ final class $$MediaAssetsTableReferences extends BaseReferences<_$AppDatabase, $MediaAssetsTable, MediaAsset> { $$MediaAssetsTableReferences(super.$_db, super.$_table, super.$_typedResult); - static MultiTypedResultKey<$ExercisesTable, List> - _exerciseImageReferencesTable(_$AppDatabase db) => - MultiTypedResultKey.fromTable( - db.exercises, - aliasName: 'media_assets__id__exercises__image_media_id', - ); - - $$ExercisesTableProcessedTableManager get exerciseImageReferences { - final manager = $$ExercisesTableTableManager( - $_db, - $_db.exercises, - ).filter((f) => f.imageMediaId.id.sqlEquals($_itemColumn('id')!)); - - final cache = $_typedResult.readTableOrNull( - _exerciseImageReferencesTable($_db), - ); - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache), - ); - } - static MultiTypedResultKey<$ExercisesTable, List> _exerciseVideoReferencesTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( @@ -22025,6 +22807,24 @@ final class $$MediaAssetsTableReferences ); } + static MultiTypedResultKey<$ExerciseImagesTable, List> + _exerciseImagesRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.exerciseImages, + aliasName: 'media_assets__id__exercise_images__media_asset_id', + ); + + $$ExerciseImagesTableProcessedTableManager get exerciseImagesRefs { + final manager = $$ExerciseImagesTableTableManager( + $_db, + $_db.exerciseImages, + ).filter((f) => f.mediaAssetId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_exerciseImagesRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + static MultiTypedResultKey<$ProgramExercisesTable, List> _programExerciseImageSnapshotReferencesTable( _$AppDatabase db, @@ -22192,14 +22992,14 @@ class $$MediaAssetsTableFilterComposer builder: (column) => ColumnFilters(column), ); - Expression exerciseImageReferences( + Expression exerciseVideoReferences( Expression Function($$ExercisesTableFilterComposer f) f, ) { final $$ExercisesTableFilterComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.id, referencedTable: $db.exercises, - getReferencedColumn: (t) => t.imageMediaId, + getReferencedColumn: (t) => t.videoMediaId, builder: ( joinBuilder, { @@ -22217,22 +23017,22 @@ class $$MediaAssetsTableFilterComposer return f(composer); } - Expression exerciseVideoReferences( - Expression Function($$ExercisesTableFilterComposer f) f, + Expression exerciseImagesRefs( + Expression Function($$ExerciseImagesTableFilterComposer f) f, ) { - final $$ExercisesTableFilterComposer composer = $composerBuilder( + final $$ExerciseImagesTableFilterComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.id, - referencedTable: $db.exercises, - getReferencedColumn: (t) => t.videoMediaId, + referencedTable: $db.exerciseImages, + getReferencedColumn: (t) => t.mediaAssetId, builder: ( joinBuilder, { $addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer, - }) => $$ExercisesTableFilterComposer( + }) => $$ExerciseImagesTableFilterComposer( $db: $db, - $table: $db.exercises, + $table: $db.exerciseImages, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -22496,14 +23296,14 @@ class $$MediaAssetsTableAnnotationComposer builder: (column) => column, ); - Expression exerciseImageReferences( + Expression exerciseVideoReferences( Expression Function($$ExercisesTableAnnotationComposer a) f, ) { final $$ExercisesTableAnnotationComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.id, referencedTable: $db.exercises, - getReferencedColumn: (t) => t.imageMediaId, + getReferencedColumn: (t) => t.videoMediaId, builder: ( joinBuilder, { @@ -22521,22 +23321,22 @@ class $$MediaAssetsTableAnnotationComposer return f(composer); } - Expression exerciseVideoReferences( - Expression Function($$ExercisesTableAnnotationComposer a) f, + Expression exerciseImagesRefs( + Expression Function($$ExerciseImagesTableAnnotationComposer a) f, ) { - final $$ExercisesTableAnnotationComposer composer = $composerBuilder( + final $$ExerciseImagesTableAnnotationComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.id, - referencedTable: $db.exercises, - getReferencedColumn: (t) => t.videoMediaId, + referencedTable: $db.exerciseImages, + getReferencedColumn: (t) => t.mediaAssetId, builder: ( joinBuilder, { $addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer, - }) => $$ExercisesTableAnnotationComposer( + }) => $$ExerciseImagesTableAnnotationComposer( $db: $db, - $table: $db.exercises, + $table: $db.exerciseImages, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -22611,8 +23411,8 @@ class $$MediaAssetsTableTableManager (MediaAsset, $$MediaAssetsTableReferences), MediaAsset, PrefetchHooks Function({ - bool exerciseImageReferences, bool exerciseVideoReferences, + bool exerciseImagesRefs, bool programExerciseImageSnapshotReferences, bool programExerciseVideoSnapshotReferences, }) @@ -22734,16 +23534,16 @@ class $$MediaAssetsTableTableManager .toList(), prefetchHooksCallback: ({ - exerciseImageReferences = false, exerciseVideoReferences = false, + exerciseImagesRefs = false, programExerciseImageSnapshotReferences = false, programExerciseVideoSnapshotReferences = false, }) { return PrefetchHooks( db: db, explicitlyWatchedTables: [ - if (exerciseImageReferences) db.exercises, if (exerciseVideoReferences) db.exercises, + if (exerciseImagesRefs) db.exerciseImages, if (programExerciseImageSnapshotReferences) db.programExercises, if (programExerciseVideoSnapshotReferences) @@ -22752,27 +23552,6 @@ class $$MediaAssetsTableTableManager addJoins: null, getPrefetchedDataCallback: (items) async { return [ - if (exerciseImageReferences) - await $_getPrefetchedData< - MediaAsset, - $MediaAssetsTable, - Exercise - >( - currentTable: table, - referencedTable: $$MediaAssetsTableReferences - ._exerciseImageReferencesTable(db), - managerFromTypedResult: (p0) => - $$MediaAssetsTableReferences( - db, - table, - p0, - ).exerciseImageReferences, - referencedItemsForCurrentItem: - (item, referencedItems) => referencedItems.where( - (e) => e.imageMediaId == item.id, - ), - typedResults: items, - ), if (exerciseVideoReferences) await $_getPrefetchedData< MediaAsset, @@ -22794,6 +23573,27 @@ class $$MediaAssetsTableTableManager ), typedResults: items, ), + if (exerciseImagesRefs) + await $_getPrefetchedData< + MediaAsset, + $MediaAssetsTable, + ExerciseImage + >( + currentTable: table, + referencedTable: $$MediaAssetsTableReferences + ._exerciseImagesRefsTable(db), + managerFromTypedResult: (p0) => + $$MediaAssetsTableReferences( + db, + table, + p0, + ).exerciseImagesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.mediaAssetId == item.id, + ), + typedResults: items, + ), if (programExerciseImageSnapshotReferences) await $_getPrefetchedData< MediaAsset, @@ -22859,8 +23659,8 @@ typedef $$MediaAssetsTableProcessedTableManager = (MediaAsset, $$MediaAssetsTableReferences), MediaAsset, PrefetchHooks Function({ - bool exerciseImageReferences, bool exerciseVideoReferences, + bool exerciseImagesRefs, bool programExerciseImageSnapshotReferences, bool programExerciseVideoSnapshotReferences, }) @@ -22880,7 +23680,6 @@ typedef $$ExercisesTableCreateCompanionBuilder = Value remoteRevision, required String name, Value description, - Value imageMediaId, Value videoMediaId, required bool hasTimeMeasure, required bool hasRepsMeasure, @@ -22910,7 +23709,6 @@ typedef $$ExercisesTableUpdateCompanionBuilder = Value remoteRevision, Value name, Value description, - Value imageMediaId, Value videoMediaId, Value hasTimeMeasure, Value hasRepsMeasure, @@ -22930,23 +23728,6 @@ final class $$ExercisesTableReferences extends BaseReferences<_$AppDatabase, $ExercisesTable, Exercise> { $$ExercisesTableReferences(super.$_db, super.$_table, super.$_typedResult); - static $MediaAssetsTable _imageMediaIdTable(_$AppDatabase db) => - db.mediaAssets.createAlias('exercises__image_media_id__media_assets__id'); - - $$MediaAssetsTableProcessedTableManager? get imageMediaId { - final $_column = $_itemColumn('image_media_id'); - if ($_column == null) return null; - final manager = $$MediaAssetsTableTableManager( - $_db, - $_db.mediaAssets, - ).filter((f) => f.id.sqlEquals($_column)); - final item = $_typedResult.readTableOrNull(_imageMediaIdTable($_db)); - if (item == null) return manager; - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: [item]), - ); - } - static $MediaAssetsTable _videoMediaIdTable(_$AppDatabase db) => db.mediaAssets.createAlias('exercises__video_media_id__media_assets__id'); @@ -22964,6 +23745,24 @@ final class $$ExercisesTableReferences ); } + static MultiTypedResultKey<$ExerciseImagesTable, List> + _exerciseImagesRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.exerciseImages, + aliasName: 'exercises__id__exercise_images__exercise_id', + ); + + $$ExerciseImagesTableProcessedTableManager get exerciseImagesRefs { + final manager = $$ExerciseImagesTableTableManager( + $_db, + $_db.exerciseImages, + ).filter((f) => f.exerciseId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_exerciseImagesRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + static MultiTypedResultKey<$ProgramExercisesTable, List> _programExercisesRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( db.programExercises, @@ -23114,29 +23913,6 @@ class $$ExercisesTableFilterComposer builder: (column) => ColumnFilters(column), ); - $$MediaAssetsTableFilterComposer get imageMediaId { - final $$MediaAssetsTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.imageMediaId, - referencedTable: $db.mediaAssets, - getReferencedColumn: (t) => t.id, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$MediaAssetsTableFilterComposer( - $db: $db, - $table: $db.mediaAssets, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return composer; - } - $$MediaAssetsTableFilterComposer get videoMediaId { final $$MediaAssetsTableFilterComposer composer = $composerBuilder( composer: this, @@ -23160,6 +23936,31 @@ class $$ExercisesTableFilterComposer return composer; } + Expression exerciseImagesRefs( + Expression Function($$ExerciseImagesTableFilterComposer f) f, + ) { + final $$ExerciseImagesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.exerciseImages, + getReferencedColumn: (t) => t.exerciseId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ExerciseImagesTableFilterComposer( + $db: $db, + $table: $db.exerciseImages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + Expression programExercisesRefs( Expression Function($$ProgramExercisesTableFilterComposer f) f, ) { @@ -23315,29 +24116,6 @@ class $$ExercisesTableOrderingComposer builder: (column) => ColumnOrderings(column), ); - $$MediaAssetsTableOrderingComposer get imageMediaId { - final $$MediaAssetsTableOrderingComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.imageMediaId, - referencedTable: $db.mediaAssets, - getReferencedColumn: (t) => t.id, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$MediaAssetsTableOrderingComposer( - $db: $db, - $table: $db.mediaAssets, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return composer; - } - $$MediaAssetsTableOrderingComposer get videoMediaId { final $$MediaAssetsTableOrderingComposer composer = $composerBuilder( composer: this, @@ -23477,29 +24255,6 @@ class $$ExercisesTableAnnotationComposer builder: (column) => column, ); - $$MediaAssetsTableAnnotationComposer get imageMediaId { - final $$MediaAssetsTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.imageMediaId, - referencedTable: $db.mediaAssets, - getReferencedColumn: (t) => t.id, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$MediaAssetsTableAnnotationComposer( - $db: $db, - $table: $db.mediaAssets, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return composer; - } - $$MediaAssetsTableAnnotationComposer get videoMediaId { final $$MediaAssetsTableAnnotationComposer composer = $composerBuilder( composer: this, @@ -23523,6 +24278,31 @@ class $$ExercisesTableAnnotationComposer return composer; } + Expression exerciseImagesRefs( + Expression Function($$ExerciseImagesTableAnnotationComposer a) f, + ) { + final $$ExerciseImagesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.exerciseImages, + getReferencedColumn: (t) => t.exerciseId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ExerciseImagesTableAnnotationComposer( + $db: $db, + $table: $db.exerciseImages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + Expression programExercisesRefs( Expression Function($$ProgramExercisesTableAnnotationComposer a) f, ) { @@ -23563,8 +24343,8 @@ class $$ExercisesTableTableManager (Exercise, $$ExercisesTableReferences), Exercise, PrefetchHooks Function({ - bool imageMediaId, bool videoMediaId, + bool exerciseImagesRefs, bool programExercisesRefs, }) > { @@ -23594,7 +24374,6 @@ class $$ExercisesTableTableManager Value remoteRevision = const Value.absent(), Value name = const Value.absent(), Value description = const Value.absent(), - Value imageMediaId = const Value.absent(), Value videoMediaId = const Value.absent(), Value hasTimeMeasure = const Value.absent(), Value hasRepsMeasure = const Value.absent(), @@ -23622,7 +24401,6 @@ class $$ExercisesTableTableManager remoteRevision: remoteRevision, name: name, description: description, - imageMediaId: imageMediaId, videoMediaId: videoMediaId, hasTimeMeasure: hasTimeMeasure, hasRepsMeasure: hasRepsMeasure, @@ -23652,7 +24430,6 @@ class $$ExercisesTableTableManager Value remoteRevision = const Value.absent(), required String name, Value description = const Value.absent(), - Value imageMediaId = const Value.absent(), Value videoMediaId = const Value.absent(), required bool hasTimeMeasure, required bool hasRepsMeasure, @@ -23680,7 +24457,6 @@ class $$ExercisesTableTableManager remoteRevision: remoteRevision, name: name, description: description, - imageMediaId: imageMediaId, videoMediaId: videoMediaId, hasTimeMeasure: hasTimeMeasure, hasRepsMeasure: hasRepsMeasure, @@ -23705,13 +24481,14 @@ class $$ExercisesTableTableManager .toList(), prefetchHooksCallback: ({ - imageMediaId = false, videoMediaId = false, + exerciseImagesRefs = false, programExercisesRefs = false, }) { return PrefetchHooks( db: db, explicitlyWatchedTables: [ + if (exerciseImagesRefs) db.exerciseImages, if (programExercisesRefs) db.programExercises, ], addJoins: @@ -23730,19 +24507,6 @@ class $$ExercisesTableTableManager dynamic > >(state) { - if (imageMediaId) { - state = - state.withJoin( - currentTable: table, - currentColumn: table.imageMediaId, - referencedTable: $$ExercisesTableReferences - ._imageMediaIdTable(db), - referencedColumn: $$ExercisesTableReferences - ._imageMediaIdTable(db) - .id, - ) - as T; - } if (videoMediaId) { state = state.withJoin( @@ -23761,6 +24525,27 @@ class $$ExercisesTableTableManager }, getPrefetchedDataCallback: (items) async { return [ + if (exerciseImagesRefs) + await $_getPrefetchedData< + Exercise, + $ExercisesTable, + ExerciseImage + >( + currentTable: table, + referencedTable: $$ExercisesTableReferences + ._exerciseImagesRefsTable(db), + managerFromTypedResult: (p0) => + $$ExercisesTableReferences( + db, + table, + p0, + ).exerciseImagesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.exerciseId == item.id, + ), + typedResults: items, + ), if (programExercisesRefs) await $_getPrefetchedData< Exercise, @@ -23803,11 +24588,606 @@ typedef $$ExercisesTableProcessedTableManager = (Exercise, $$ExercisesTableReferences), Exercise, PrefetchHooks Function({ - bool imageMediaId, bool videoMediaId, + bool exerciseImagesRefs, bool programExercisesRefs, }) >; +typedef $$ExerciseImagesTableCreateCompanionBuilder = + ExerciseImagesCompanion Function({ + required String id, + required DateTime createdAt, + required DateTime updatedAt, + Value deletedAt, + Value schemaVersion, + required String syncState, + required int localRevision, + required String originDeviceId, + Value futureOwnerProfileId, + Value lastSyncedAt, + Value remoteRevision, + required String exerciseId, + required String mediaAssetId, + required int position, + Value rowid, + }); +typedef $$ExerciseImagesTableUpdateCompanionBuilder = + ExerciseImagesCompanion Function({ + Value id, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value schemaVersion, + Value syncState, + Value localRevision, + Value originDeviceId, + Value futureOwnerProfileId, + Value lastSyncedAt, + Value remoteRevision, + Value exerciseId, + Value mediaAssetId, + Value position, + Value rowid, + }); + +final class $$ExerciseImagesTableReferences + extends BaseReferences<_$AppDatabase, $ExerciseImagesTable, ExerciseImage> { + $$ExerciseImagesTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static $ExercisesTable _exerciseIdTable(_$AppDatabase db) => + db.exercises.createAlias('exercise_images__exercise_id__exercises__id'); + + $$ExercisesTableProcessedTableManager get exerciseId { + final $_column = $_itemColumn('exercise_id')!; + + final manager = $$ExercisesTableTableManager( + $_db, + $_db.exercises, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_exerciseIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $MediaAssetsTable _mediaAssetIdTable(_$AppDatabase db) => db + .mediaAssets + .createAlias('exercise_images__media_asset_id__media_assets__id'); + + $$MediaAssetsTableProcessedTableManager get mediaAssetId { + final $_column = $_itemColumn('media_asset_id')!; + + final manager = $$MediaAssetsTableTableManager( + $_db, + $_db.mediaAssets, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_mediaAssetIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$ExerciseImagesTableFilterComposer + extends Composer<_$AppDatabase, $ExerciseImagesTable> { + $$ExerciseImagesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get schemaVersion => $composableBuilder( + column: $table.schemaVersion, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncState => $composableBuilder( + column: $table.syncState, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get localRevision => $composableBuilder( + column: $table.localRevision, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get originDeviceId => $composableBuilder( + column: $table.originDeviceId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get futureOwnerProfileId => $composableBuilder( + column: $table.futureOwnerProfileId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get lastSyncedAt => $composableBuilder( + column: $table.lastSyncedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get remoteRevision => $composableBuilder( + column: $table.remoteRevision, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get position => $composableBuilder( + column: $table.position, + builder: (column) => ColumnFilters(column), + ); + + $$ExercisesTableFilterComposer get exerciseId { + final $$ExercisesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.exerciseId, + referencedTable: $db.exercises, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ExercisesTableFilterComposer( + $db: $db, + $table: $db.exercises, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$MediaAssetsTableFilterComposer get mediaAssetId { + final $$MediaAssetsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.mediaAssetId, + referencedTable: $db.mediaAssets, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$MediaAssetsTableFilterComposer( + $db: $db, + $table: $db.mediaAssets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ExerciseImagesTableOrderingComposer + extends Composer<_$AppDatabase, $ExerciseImagesTable> { + $$ExerciseImagesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get schemaVersion => $composableBuilder( + column: $table.schemaVersion, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncState => $composableBuilder( + column: $table.syncState, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get localRevision => $composableBuilder( + column: $table.localRevision, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get originDeviceId => $composableBuilder( + column: $table.originDeviceId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get futureOwnerProfileId => $composableBuilder( + column: $table.futureOwnerProfileId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get lastSyncedAt => $composableBuilder( + column: $table.lastSyncedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get remoteRevision => $composableBuilder( + column: $table.remoteRevision, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get position => $composableBuilder( + column: $table.position, + builder: (column) => ColumnOrderings(column), + ); + + $$ExercisesTableOrderingComposer get exerciseId { + final $$ExercisesTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.exerciseId, + referencedTable: $db.exercises, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ExercisesTableOrderingComposer( + $db: $db, + $table: $db.exercises, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$MediaAssetsTableOrderingComposer get mediaAssetId { + final $$MediaAssetsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.mediaAssetId, + referencedTable: $db.mediaAssets, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$MediaAssetsTableOrderingComposer( + $db: $db, + $table: $db.mediaAssets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ExerciseImagesTableAnnotationComposer + extends Composer<_$AppDatabase, $ExerciseImagesTable> { + $$ExerciseImagesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + GeneratedColumn get schemaVersion => $composableBuilder( + column: $table.schemaVersion, + builder: (column) => column, + ); + + GeneratedColumn get syncState => + $composableBuilder(column: $table.syncState, builder: (column) => column); + + GeneratedColumn get localRevision => $composableBuilder( + column: $table.localRevision, + builder: (column) => column, + ); + + GeneratedColumn get originDeviceId => $composableBuilder( + column: $table.originDeviceId, + builder: (column) => column, + ); + + GeneratedColumn get futureOwnerProfileId => $composableBuilder( + column: $table.futureOwnerProfileId, + builder: (column) => column, + ); + + GeneratedColumn get lastSyncedAt => $composableBuilder( + column: $table.lastSyncedAt, + builder: (column) => column, + ); + + GeneratedColumn get remoteRevision => $composableBuilder( + column: $table.remoteRevision, + builder: (column) => column, + ); + + GeneratedColumn get position => + $composableBuilder(column: $table.position, builder: (column) => column); + + $$ExercisesTableAnnotationComposer get exerciseId { + final $$ExercisesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.exerciseId, + referencedTable: $db.exercises, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ExercisesTableAnnotationComposer( + $db: $db, + $table: $db.exercises, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$MediaAssetsTableAnnotationComposer get mediaAssetId { + final $$MediaAssetsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.mediaAssetId, + referencedTable: $db.mediaAssets, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$MediaAssetsTableAnnotationComposer( + $db: $db, + $table: $db.mediaAssets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ExerciseImagesTableTableManager + extends + RootTableManager< + _$AppDatabase, + $ExerciseImagesTable, + ExerciseImage, + $$ExerciseImagesTableFilterComposer, + $$ExerciseImagesTableOrderingComposer, + $$ExerciseImagesTableAnnotationComposer, + $$ExerciseImagesTableCreateCompanionBuilder, + $$ExerciseImagesTableUpdateCompanionBuilder, + (ExerciseImage, $$ExerciseImagesTableReferences), + ExerciseImage, + PrefetchHooks Function({bool exerciseId, bool mediaAssetId}) + > { + $$ExerciseImagesTableTableManager( + _$AppDatabase db, + $ExerciseImagesTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ExerciseImagesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ExerciseImagesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ExerciseImagesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value schemaVersion = const Value.absent(), + Value syncState = const Value.absent(), + Value localRevision = const Value.absent(), + Value originDeviceId = const Value.absent(), + Value futureOwnerProfileId = const Value.absent(), + Value lastSyncedAt = const Value.absent(), + Value remoteRevision = const Value.absent(), + Value exerciseId = const Value.absent(), + Value mediaAssetId = const Value.absent(), + Value position = const Value.absent(), + Value rowid = const Value.absent(), + }) => ExerciseImagesCompanion( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + schemaVersion: schemaVersion, + syncState: syncState, + localRevision: localRevision, + originDeviceId: originDeviceId, + futureOwnerProfileId: futureOwnerProfileId, + lastSyncedAt: lastSyncedAt, + remoteRevision: remoteRevision, + exerciseId: exerciseId, + mediaAssetId: mediaAssetId, + position: position, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + required DateTime createdAt, + required DateTime updatedAt, + Value deletedAt = const Value.absent(), + Value schemaVersion = const Value.absent(), + required String syncState, + required int localRevision, + required String originDeviceId, + Value futureOwnerProfileId = const Value.absent(), + Value lastSyncedAt = const Value.absent(), + Value remoteRevision = const Value.absent(), + required String exerciseId, + required String mediaAssetId, + required int position, + Value rowid = const Value.absent(), + }) => ExerciseImagesCompanion.insert( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + schemaVersion: schemaVersion, + syncState: syncState, + localRevision: localRevision, + originDeviceId: originDeviceId, + futureOwnerProfileId: futureOwnerProfileId, + lastSyncedAt: lastSyncedAt, + remoteRevision: remoteRevision, + exerciseId: exerciseId, + mediaAssetId: mediaAssetId, + position: position, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$ExerciseImagesTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({exerciseId = false, mediaAssetId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (exerciseId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.exerciseId, + referencedTable: $$ExerciseImagesTableReferences + ._exerciseIdTable(db), + referencedColumn: + $$ExerciseImagesTableReferences + ._exerciseIdTable(db) + .id, + ) + as T; + } + if (mediaAssetId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.mediaAssetId, + referencedTable: $$ExerciseImagesTableReferences + ._mediaAssetIdTable(db), + referencedColumn: + $$ExerciseImagesTableReferences + ._mediaAssetIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$ExerciseImagesTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $ExerciseImagesTable, + ExerciseImage, + $$ExerciseImagesTableFilterComposer, + $$ExerciseImagesTableOrderingComposer, + $$ExerciseImagesTableAnnotationComposer, + $$ExerciseImagesTableCreateCompanionBuilder, + $$ExerciseImagesTableUpdateCompanionBuilder, + (ExerciseImage, $$ExerciseImagesTableReferences), + ExerciseImage, + PrefetchHooks Function({bool exerciseId, bool mediaAssetId}) + >; typedef $$ProgramsTableCreateCompanionBuilder = ProgramsCompanion Function({ required String id, @@ -28914,6 +30294,8 @@ class $AppDatabaseManager { $$MediaAssetsTableTableManager(_db, _db.mediaAssets); $$ExercisesTableTableManager get exercises => $$ExercisesTableTableManager(_db, _db.exercises); + $$ExerciseImagesTableTableManager get exerciseImages => + $$ExerciseImagesTableTableManager(_db, _db.exerciseImages); $$ProgramsTableTableManager get programs => $$ProgramsTableTableManager(_db, _db.programs); $$ProgramExercisesTableTableManager get programExercises => diff --git a/lib/infrastructure/local/drift_repositories.dart b/lib/infrastructure/local/drift_repositories.dart index 2137681..4137bcb 100644 --- a/lib/infrastructure/local/drift_repositories.dart +++ b/lib/infrastructure/local/drift_repositories.dart @@ -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 = []; + 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 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> _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 _upsertWithChangeLog({ }); } +Future _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 _softDeleteExerciseImageRows( + db.AppDatabase database, + List 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 _operationForMutation({ required db.AppDatabase database, required String tableName, @@ -958,7 +1076,6 @@ db.ExercisesCompanion _exerciseCompanion(domain.Exercise exercise) { remoteRevision: values[10] as Value, 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 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, diff --git a/lib/infrastructure/local/tables.dart b/lib/infrastructure/local/tables.dart index b8e95ef..2c9a705 100644 --- a/lib/infrastructure/local/tables.dart +++ b/lib/infrastructure/local/tables.dart @@ -55,10 +55,6 @@ class Exercises extends SyncableTable { TextColumn get name => text().withLength(min: 1)(); TextColumn get description => text().nullable()(); - @ReferenceName('exerciseImageReferences') - TextColumn get imageMediaId => - text().nullable().references(MediaAssets, #id)(); - @ReferenceName('exerciseVideoReferences') TextColumn get videoMediaId => text().nullable().references(MediaAssets, #id)(); @@ -92,6 +88,22 @@ class Exercises extends SyncableTable { ]; } +class ExerciseImages extends SyncableTable { + @override + String get tableName => 'exercise_images'; + + TextColumn get exerciseId => text().references(Exercises, #id)(); + TextColumn get mediaAssetId => text().references(MediaAssets, #id)(); + IntColumn get position => integer()(); + + @override + List get customConstraints => [ + 'UNIQUE (exercise_id, position)', + 'UNIQUE (exercise_id, media_asset_id)', + 'CHECK (position >= 0 AND position < 5)', + ]; +} + class Programs extends SyncableTable { @override String get tableName => 'programs'; diff --git a/lib/presentation/exercise_library_screen.dart b/lib/presentation/exercise_library_screen.dart index 169844c..2b71eee 100644 --- a/lib/presentation/exercise_library_screen.dart +++ b/lib/presentation/exercise_library_screen.dart @@ -3,6 +3,7 @@ import 'package:image_picker/image_picker.dart'; import '../application/application.dart'; import '../domain/domain.dart'; +import 'theme.dart'; abstract interface class MediaSourcePicker { Future pickPath(MediaKind kind); @@ -245,9 +246,9 @@ final class _ExerciseLibraryScreenState extends State { void _showSnackBar(String message) { if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(message))); + final messenger = ScaffoldMessenger.of(context); + messenger.hideCurrentSnackBar(); + messenger.showSnackBar(SnackBar(content: Text(message))); } } @@ -355,9 +356,9 @@ final class _ExerciseFormScreenState extends State { late final TextEditingController _defaultRepsController; late final TextEditingController _defaultScoreController; late final TextEditingController _defaultScoreTimeController; - String? _imageMediaId; + late List _imageMediaIds; String? _videoMediaId; - String? _selectedImageName; + final _selectedImageNamesById = {}; String? _selectedVideoName; var _hasTime = true; var _hasReps = false; @@ -393,7 +394,7 @@ final class _ExerciseFormScreenState extends State { _millisecondsToSeconds(exercise?.defaultTargetScoreTimeMs), ), ); - _imageMediaId = exercise?.imageMediaId; + _imageMediaIds = List.of(exercise?.imageMediaIds ?? const []); _videoMediaId = exercise?.videoMediaId; _hasTime = exercise?.hasTimeMeasure ?? true; _hasReps = exercise?.hasRepsMeasure ?? false; @@ -446,13 +447,12 @@ final class _ExerciseFormScreenState extends State { style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), - _MediaImportField( - label: 'Image', - selectedFileName: _selectedImageName, - imported: _imageMediaId != null, + _ImageGalleryField( + imageMediaIds: _imageMediaIds, + imageNamesById: _selectedImageNamesById, importing: _importingImage, - actionLabel: 'Choisir une image', - onPick: () => _importMedia(MediaKind.image), + onAdd: _importImage, + onRemove: _removeImage, ), const SizedBox(height: 8), _MediaImportField( @@ -461,7 +461,7 @@ final class _ExerciseFormScreenState extends State { imported: _videoMediaId != null, importing: _importingVideo, actionLabel: 'Choisir une vidéo', - onPick: () => _importMedia(MediaKind.video), + onPick: _importVideo, ), const SizedBox(height: 24), Text( @@ -668,43 +668,71 @@ final class _ExerciseFormScreenState extends State { }); } - Future _importMedia(MediaKind kind) async { - final sourcePath = await widget.mediaPicker.pickPath(kind); + Future _importImage() async { + if (_imageMediaIds.length >= 5) { + _showSnackBar('Maximum 5 images par exercice.'); + return; + } + final sourcePath = await widget.mediaPicker.pickPath(MediaKind.image); + if (sourcePath == null) { + return; + } + final fileName = _fileNameFromPath(sourcePath); + setState(() => _importingImage = true); + try { + final asset = await widget.mediaUseCases.importMedia( + sourcePath: sourcePath, + kind: MediaKind.image, + ); + if (!mounted) return; + setState(() { + _imageMediaIds = [..._imageMediaIds, asset.metadata.id]; + _selectedImageNamesById[asset.metadata.id] = fileName; + }); + _showSnackBar('Image ajoutée.'); + } on Exception catch (error) { + _showSnackBar(error.toString()); + } finally { + if (mounted) { + setState(() => _importingImage = false); + } + } + } + + void _removeImage(String mediaAssetId) { + setState(() { + _imageMediaIds = _imageMediaIds + .where((imageId) => imageId != mediaAssetId) + .toList(); + _selectedImageNamesById.remove(mediaAssetId); + }); + } + + Future _importVideo() async { + final sourcePath = await widget.mediaPicker.pickPath(MediaKind.video); if (sourcePath == null) { return; } final fileName = _fileNameFromPath(sourcePath); setState(() { - if (kind == MediaKind.image) { - _importingImage = true; - _selectedImageName = fileName; - } else { - _importingVideo = true; - _selectedVideoName = fileName; - } + _importingVideo = true; + _selectedVideoName = fileName; }); try { final asset = await widget.mediaUseCases.importMedia( sourcePath: sourcePath, - kind: kind, + kind: MediaKind.video, ); if (!mounted) return; setState(() { - if (kind == MediaKind.image) { - _imageMediaId = asset.metadata.id; - } else { - _videoMediaId = asset.metadata.id; - } + _videoMediaId = asset.metadata.id; }); - _showSnackBar( - kind == MediaKind.image ? 'Image importée.' : 'Vidéo importée.', - ); + _showSnackBar('Vidéo importée.'); } on Exception catch (error) { _showSnackBar(error.toString()); } finally { if (mounted) { setState(() { - _importingImage = false; _importingVideo = false; }); } @@ -766,7 +794,7 @@ final class _ExerciseFormScreenState extends State { await widget.exerciseUseCases.create( name: _nameController.text.trim(), description: _optionalText(_descriptionController), - imageMediaId: _imageMediaId, + imageMediaIds: _imageMediaIds, videoMediaId: _videoMediaId, hasTimeMeasure: _hasTime, hasRepsMeasure: _hasReps, @@ -784,7 +812,7 @@ final class _ExerciseFormScreenState extends State { id: exercise.metadata.id, name: _nameController.text.trim(), description: _optionalText(_descriptionController), - imageMediaId: _imageMediaId, + imageMediaIds: _imageMediaIds, videoMediaId: _videoMediaId, hasTimeMeasure: _hasTime, hasRepsMeasure: _hasReps, @@ -937,6 +965,130 @@ final class _MeasureSwitch extends StatelessWidget { } } +final class _ImageGalleryField extends StatelessWidget { + const _ImageGalleryField({ + required this.imageMediaIds, + required this.imageNamesById, + required this.importing, + required this.onAdd, + required this.onRemove, + }); + + final List imageMediaIds; + final Map imageNamesById; + final bool importing; + final VoidCallback onAdd; + final ValueChanged onRemove; + + @override + Widget build(BuildContext context) { + final canAdd = imageMediaIds.length < 5; + return InputDecorator( + decoration: const InputDecoration(labelText: 'Images'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (imageMediaIds.isEmpty) + const Text('Aucune image sélectionnée') + else + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (var index = 0; index < imageMediaIds.length; index++) + _ImageThumbnail( + mediaAssetId: imageMediaIds[index], + label: + imageNamesById[imageMediaIds[index]] ?? + 'Image ${index + 1}', + onRemove: onRemove, + ), + ], + ), + const SizedBox(height: 12), + Align( + alignment: Alignment.centerLeft, + child: OutlinedButton.icon( + onPressed: importing ? null : onAdd, + icon: importing + ? const SizedBox.square( + dimension: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.add_photo_alternate_outlined), + label: const Text('Ajouter une image'), + ), + ), + if (!canAdd) ...[ + const SizedBox(height: 8), + Text( + 'Maximum 5 images par exercice.', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ], + ), + ); + } +} + +final class _ImageThumbnail extends StatelessWidget { + const _ImageThumbnail({ + required this.mediaAssetId, + required this.label, + required this.onRemove, + }); + + final String mediaAssetId; + final String label; + final ValueChanged onRemove; + + @override + Widget build(BuildContext context) { + final tokens = courtBlazerTokensOf(context); + return SizedBox( + width: 104, + child: Stack( + children: [ + Container( + height: 104, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + border: Border.all(color: tokens.border), + color: Theme.of(context).colorScheme.surface, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.image_outlined), + const SizedBox(height: 8), + Text( + label, + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.labelSmall, + ), + ], + ), + ), + Positioned( + top: 2, + right: 2, + child: IconButton.filledTonal( + tooltip: 'Supprimer l’image', + visualDensity: VisualDensity.compact, + icon: const Icon(Icons.close, size: 18), + onPressed: () => onRemove(mediaAssetId), + ), + ), + ], + ), + ); + } +} + final class _MediaImportField extends StatelessWidget { const _MediaImportField({ required this.label, diff --git a/test/application/use_cases_test.dart b/test/application/use_cases_test.dart index e7d47e6..1beef85 100644 --- a/test/application/use_cases_test.dart +++ b/test/application/use_cases_test.dart @@ -156,6 +156,75 @@ void main() { ); }); + test('Exercise image gallery rejects a sixth image', () async { + final repository = _FakeExerciseRepository() + ..exercise = Exercise( + metadata: _metadata('exercise-1'), + name: 'Squat', + imageMediaIds: const [ + 'media-1', + 'media-2', + 'media-3', + 'media-4', + 'media-5', + ], + hasTimeMeasure: false, + hasRepsMeasure: true, + hasScoreMeasure: false, + defaultTargetReps: 10, + ); + final useCase = _exerciseUseCase(repository); + + await expectLater( + useCase.addImage(exerciseId: 'exercise-1', mediaAssetId: 'media-6'), + throwsA(isA()), + ); + }); + + test('Exercise image gallery reorders images', () async { + final repository = _FakeExerciseRepository() + ..exercise = Exercise( + metadata: _metadata('exercise-1'), + name: 'Squat', + imageMediaIds: const ['media-1', 'media-2', 'media-3'], + hasTimeMeasure: false, + hasRepsMeasure: true, + hasScoreMeasure: false, + defaultTargetReps: 10, + ); + final useCase = _exerciseUseCase(repository); + + final updated = await useCase.reorderImages( + exerciseId: 'exercise-1', + orderedMediaIds: const ['media-3', 'media-1', 'media-2'], + ); + + expect(updated.imageMediaIds, ['media-3', 'media-1', 'media-2']); + expect(repository.saved.single.imageMediaIds, updated.imageMediaIds); + }); + + test('Exercise image gallery removes an image', () async { + final repository = _FakeExerciseRepository() + ..exercise = Exercise( + metadata: _metadata('exercise-1'), + name: 'Squat', + imageMediaIds: const ['media-1', 'media-2', 'media-3'], + hasTimeMeasure: false, + hasRepsMeasure: true, + hasScoreMeasure: false, + defaultTargetReps: 10, + ); + final useCase = _exerciseUseCase(repository); + + final updated = await useCase.removeImage( + exerciseId: 'exercise-1', + mediaAssetId: 'media-2', + ); + + expect(updated.imageMediaIds, ['media-1', 'media-3']); + expect(updated.imageMediaId, 'media-1'); + }); + test('delete exercise removes it from referencing programs', () async { final exerciseRepository = _FakeExerciseRepository() ..exercise = Exercise( diff --git a/test/presentation/exercise_library_screen_test.dart b/test/presentation/exercise_library_screen_test.dart index 67c006d..497faa1 100644 --- a/test/presentation/exercise_library_screen_test.dart +++ b/test/presentation/exercise_library_screen_test.dart @@ -132,23 +132,90 @@ void main() { home: ExerciseFormScreen( exerciseUseCases: _exerciseUseCases(exerciseRepository), mediaUseCases: _mediaUseCases(exerciseRepository, storage: storage), - mediaPicker: const _FakeMediaPicker( - imagePath: '/tmp/imports/photo.png', - ), + mediaPicker: _FakeMediaPicker(imagePath: '/tmp/imports/photo.png'), ), ), ); - await tester.tap(find.text('Choisir une image')); + await tester.tap(find.text('Ajouter une image')); await tester.pump(); expect(storage.imports.single.sourcePath, '/tmp/imports/photo.png'); expect(storage.imports.single.kind, MediaKind.image); expect(find.text('photo.png'), findsOneWidget); - expect(find.text('Image importée.'), findsOneWidget); + expect(find.text('Image ajoutée.'), findsOneWidget); }, ); + testWidgets('ajouter des images bloque la sixième image', (tester) async { + final exerciseRepository = _FakeExerciseRepository(); + final storage = _FakeMediaStorage(); + + await tester.binding.setSurfaceSize(const Size(400, 1200)); + addTearDown(() => tester.binding.setSurfaceSize(null)); + + await tester.pumpWidget( + MaterialApp( + home: ExerciseFormScreen( + exerciseUseCases: _exerciseUseCases(exerciseRepository), + mediaUseCases: _mediaUseCases(exerciseRepository, storage: storage), + mediaPicker: _FakeMediaPicker( + imagePaths: const [ + '/tmp/imports/photo-1.png', + '/tmp/imports/photo-2.png', + '/tmp/imports/photo-3.png', + '/tmp/imports/photo-4.png', + '/tmp/imports/photo-5.png', + '/tmp/imports/photo-6.png', + ], + ), + ), + ), + ); + + for (var i = 0; i < 5; i++) { + await tester.tap(find.text('Ajouter une image')); + await tester.pump(); + } + + expect(storage.imports, hasLength(5)); + expect(find.text('photo-5.png'), findsOneWidget); + expect(find.text('Maximum 5 images par exercice.'), findsOneWidget); + + await tester.tap(find.text('Ajouter une image')); + await tester.pump(); + + expect(storage.imports, hasLength(5)); + expect(find.text('photo-6.png'), findsNothing); + expect(find.text('Maximum 5 images par exercice.'), findsWidgets); + }); + + testWidgets('supprimer une image la retire de la galerie', (tester) async { + final exerciseRepository = _FakeExerciseRepository(); + final storage = _FakeMediaStorage(); + + await tester.pumpWidget( + MaterialApp( + home: ExerciseFormScreen( + exerciseUseCases: _exerciseUseCases(exerciseRepository), + mediaUseCases: _mediaUseCases(exerciseRepository, storage: storage), + mediaPicker: _FakeMediaPicker(imagePath: '/tmp/imports/photo.png'), + ), + ), + ); + + await tester.tap(find.text('Ajouter une image')); + await tester.pump(); + + expect(find.text('photo.png'), findsOneWidget); + + await tester.tap(find.byTooltip('Supprimer l’image')); + await tester.pump(); + + expect(find.text('photo.png'), findsNothing); + expect(find.text('Aucune image sélectionnée'), findsOneWidget); + }); + testWidgets( 'supprimer un exercice demande confirmation puis le retire de la liste', (tester) async { @@ -244,17 +311,31 @@ MediaUseCases _mediaUseCases( } final class _FakeMediaPicker implements MediaSourcePicker { - const _FakeMediaPicker({this.imagePath}); + _FakeMediaPicker({this.imagePath, this.imagePaths = const []}); final String? imagePath; + final List imagePaths; + var _nextImagePath = 0; @override Future pickPath(MediaKind kind) async { return switch (kind) { - MediaKind.image => imagePath, + MediaKind.image => _pickImagePath(), MediaKind.video => null, }; } + + String? _pickImagePath() { + if (imagePaths.isEmpty) { + return imagePath; + } + if (_nextImagePath >= imagePaths.length) { + return null; + } + final path = imagePaths[_nextImagePath]; + _nextImagePath += 1; + return path; + } } EntityMetadata _metadata(String id) { From a3f8a1970b448fbb284408f25dd845ab8a5eee76 Mon Sep 17 00:00:00 2001 From: Blomios Date: Sat, 18 Jul 2026 14:02:49 +0200 Subject: [PATCH 2/2] =?UTF-8?q?docs(ideai):=20met=20=C3=A0=20jour=20le=20t?= =?UTF-8?q?icket=20#35=20apr=C3=A8s=20la=20galerie=20d'images=20d'exercice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .ideai/tickets/35/carnet.md | 9 ++++++--- .ideai/tickets/35/issue.md | 8 ++++---- .ideai/tickets/index.json | 4 ++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.ideai/tickets/35/carnet.md b/.ideai/tickets/35/carnet.md index 131d9dc..f5eb3a4 100644 --- a/.ideai/tickets/35/carnet.md +++ b/.ideai/tickets/35/carnet.md @@ -1,6 +1,9 @@ --- issueRef: "#35" -version: 5 -updatedBy: {"kind":"user"} -updatedAt: 1784329838645 +version: 8 +updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} +updatedAt: 1784364636506 --- +Découpage : +1. [DevBackend] Remplacer le champ unique `imageMediaId` (String?) sur Exercise par une liste ordonnée d'images (max 5) — recommandation : table Drift dédiée `exercise_images` (exerciseId, mediaAssetId, position) plutôt qu'une colonne JSON, pour rester cohérent avec le style relationnel du reste du schéma. Garde `videoMediaId` inchangé (une seule vidéo, pas concerné par ce ticket). Use case pour ajouter/retirer/réordonner une image (max 5, message d'erreur si on tente d'en ajouter une 6e). Migration Drift (schemaVersion+1). Adapter le snapshot existant (exerciseImageMediaIdSnapshot sur ProgramExercise) en conséquence si pertinent — à toi de juger si le snapshot garde juste la première image comme vignette ou la liste complète (le ticket #36 gérera l'affichage complet, mais le snapshot doit au moins rester cohérent et ne pas casser). +2. [DevFrontend] Dans le formulaire d'exercice, remplacer le sélecteur d'image unique par une gestion de galerie (ajouter jusqu'à 5 images, les voir en miniature, les retirer individuellement). Message clair si on atteint la limite de 5. \ No newline at end of file diff --git a/.ideai/tickets/35/issue.md b/.ideai/tickets/35/issue.md index 2826197..abec530 100644 --- a/.ideai/tickets/35/issue.md +++ b/.ideai/tickets/35/issue.md @@ -2,15 +2,15 @@ id: "3f1a583a-320b-43c3-9993-52d249c9c430" number: 35 title: "Proposer de mettre plsuieurs images pour un exercice" -status: "open" +status: "closed" priority: "low" sprint: null links: [] agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}] createdBy: {"kind":"user"} -updatedBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} createdAt: 1784329532159 -updatedAt: 1784329838645 -version: 5 +updatedAt: 1784364636506 +version: 8 --- J'aimerais que lors de la création d'un exercice, ils oit possible d'associer jusqu'a 5 images \ No newline at end of file diff --git a/.ideai/tickets/index.json b/.ideai/tickets/index.json index ff62137..e996d56 100644 --- a/.ideai/tickets/index.json +++ b/.ideai/tickets/index.json @@ -393,13 +393,13 @@ "issueRef": "#35", "path": "35", "title": "Proposer de mettre plsuieurs images pour un exercice", - "status": "open", + "status": "closed", "priority": "low", "sprint": null, "assignedAgentIds": [ "57695b92-24d0-4876-837c-76116e70a6ae" ], - "updatedAt": 1784329838645 + "updatedAt": 1784364636506 }, { "issueRef": "#36",