feat(exercice): icône d'exercice (ticket #38)
Étend le modèle Drift (tables.dart, app_database.dart/.g.dart), les entités/use cases/repositories et les écrans bibliothèque d'exercices et programme pour associer une icône à chaque exercice. flutter analyze propre, 72/72 tests verts, build APK debug validé. Dernier ticket du backlog. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -25,6 +25,7 @@ final class ExerciseUseCases {
|
||||
String? description,
|
||||
String? imageMediaId,
|
||||
List<String> imageMediaIds = const [],
|
||||
String? iconMediaId,
|
||||
String? videoMediaId,
|
||||
required bool hasTimeMeasure,
|
||||
required bool hasRepsMeasure,
|
||||
@ -53,6 +54,7 @@ final class ExerciseUseCases {
|
||||
name: name,
|
||||
description: description,
|
||||
imageMediaIds: _resolveExerciseImageIds(imageMediaId, imageMediaIds),
|
||||
iconMediaId: iconMediaId,
|
||||
videoMediaId: videoMediaId,
|
||||
hasTimeMeasure: hasTimeMeasure,
|
||||
hasRepsMeasure: hasRepsMeasure,
|
||||
@ -83,6 +85,7 @@ final class ExerciseUseCases {
|
||||
String? description,
|
||||
String? imageMediaId,
|
||||
Object? imageMediaIds = _useCaseUnchanged,
|
||||
Object? iconMediaId = _useCaseUnchanged,
|
||||
String? videoMediaId,
|
||||
required bool hasTimeMeasure,
|
||||
required bool hasRepsMeasure,
|
||||
@ -123,13 +126,21 @@ final class ExerciseUseCases {
|
||||
defaultTargetScore: resolvedDefaultTargetScore,
|
||||
defaultTargetScoreTimeMs: resolvedDefaultTargetScoreTimeMs,
|
||||
);
|
||||
final resolvedImageMediaIds = imageMediaIds == _useCaseUnchanged
|
||||
? _resolveExerciseImageIds(imageMediaId, exercise.imageMediaIds)
|
||||
: imageMediaIds as List<String>;
|
||||
final resolvedIconMediaId = iconMediaId == _useCaseUnchanged
|
||||
? exercise.iconMediaId != null &&
|
||||
resolvedImageMediaIds.contains(exercise.iconMediaId)
|
||||
? exercise.iconMediaId
|
||||
: null
|
||||
: iconMediaId as String?;
|
||||
final updated = exercise.copyWith(
|
||||
metadata: exercise.metadata.touch(clock.now()),
|
||||
name: name,
|
||||
description: description,
|
||||
imageMediaIds: imageMediaIds == _useCaseUnchanged
|
||||
? _resolveExerciseImageIds(imageMediaId, exercise.imageMediaIds)
|
||||
: imageMediaIds as List<String>,
|
||||
imageMediaIds: resolvedImageMediaIds,
|
||||
iconMediaId: resolvedIconMediaId,
|
||||
videoMediaId: videoMediaId,
|
||||
hasTimeMeasure: hasTimeMeasure,
|
||||
hasRepsMeasure: hasRepsMeasure,
|
||||
@ -190,6 +201,9 @@ final class ExerciseUseCases {
|
||||
imageMediaIds: exercise.imageMediaIds
|
||||
.where((imageId) => imageId != mediaAssetId)
|
||||
.toList(),
|
||||
iconMediaId: exercise.iconMediaId == mediaAssetId
|
||||
? null
|
||||
: exercise.iconMediaId,
|
||||
metadata: exercise.metadata.touch(clock.now()),
|
||||
);
|
||||
await repository.save(updated);
|
||||
@ -217,6 +231,22 @@ final class ExerciseUseCases {
|
||||
return updated;
|
||||
}
|
||||
|
||||
Future<Exercise> setIcon({
|
||||
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(
|
||||
iconMediaId: mediaAssetId,
|
||||
metadata: exercise.metadata.touch(clock.now()),
|
||||
);
|
||||
await repository.save(updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
Future<Exercise> attachVideo({
|
||||
required String exerciseId,
|
||||
required String mediaAssetId,
|
||||
|
||||
@ -126,6 +126,7 @@ final class Exercise {
|
||||
this.description,
|
||||
String? imageMediaId,
|
||||
List<String> imageMediaIds = const [],
|
||||
String? iconMediaId,
|
||||
this.videoMediaId,
|
||||
required this.hasTimeMeasure,
|
||||
required this.hasRepsMeasure,
|
||||
@ -139,10 +140,13 @@ final class Exercise {
|
||||
this.defaultTargetScoreTimeMs,
|
||||
this.archivedAt,
|
||||
}) : name = _nonBlank(name, 'Exercise name'),
|
||||
imageMediaIds = _validatedImageMediaIds(
|
||||
imageMediaIds.isEmpty && imageMediaId != null
|
||||
? [imageMediaId]
|
||||
: imageMediaIds,
|
||||
imageMediaIds = _resolvedExerciseImageMediaIds(
|
||||
imageMediaId,
|
||||
imageMediaIds,
|
||||
),
|
||||
iconMediaId = _validatedIconMediaId(
|
||||
iconMediaId,
|
||||
_resolvedExerciseImageMediaIds(imageMediaId, imageMediaIds),
|
||||
) {
|
||||
_requireAtLeastOneMeasure(
|
||||
hasTime: hasTimeMeasure,
|
||||
@ -174,6 +178,7 @@ final class Exercise {
|
||||
final String name;
|
||||
final String? description;
|
||||
final List<String> imageMediaIds;
|
||||
final String? iconMediaId;
|
||||
final String? videoMediaId;
|
||||
final bool hasTimeMeasure;
|
||||
final bool hasRepsMeasure;
|
||||
@ -206,6 +211,7 @@ final class Exercise {
|
||||
Object? description = _unchanged,
|
||||
Object? imageMediaId = _unchanged,
|
||||
Object? imageMediaIds = _unchanged,
|
||||
Object? iconMediaId = _unchanged,
|
||||
Object? videoMediaId = _unchanged,
|
||||
bool? hasTimeMeasure,
|
||||
bool? hasRepsMeasure,
|
||||
@ -219,17 +225,25 @@ final class Exercise {
|
||||
Object? defaultTargetScoreTimeMs = _unchanged,
|
||||
Object? archivedAt = _unchanged,
|
||||
}) {
|
||||
final resolvedImageMediaIds = imageMediaIds == _unchanged
|
||||
? imageMediaId == _unchanged
|
||||
? this.imageMediaIds
|
||||
: [if (imageMediaId != null) imageMediaId as String]
|
||||
: imageMediaIds as List<String>;
|
||||
final resolvedIconMediaId = iconMediaId == _unchanged
|
||||
? this.iconMediaId != null &&
|
||||
resolvedImageMediaIds.contains(this.iconMediaId)
|
||||
? this.iconMediaId
|
||||
: null
|
||||
: iconMediaId as String?;
|
||||
return Exercise(
|
||||
metadata: metadata ?? this.metadata,
|
||||
name: name ?? this.name,
|
||||
description: description == _unchanged
|
||||
? this.description
|
||||
: description as String?,
|
||||
imageMediaIds: imageMediaIds == _unchanged
|
||||
? imageMediaId == _unchanged
|
||||
? this.imageMediaIds
|
||||
: [if (imageMediaId != null) imageMediaId as String]
|
||||
: imageMediaIds as List<String>,
|
||||
imageMediaIds: resolvedImageMediaIds,
|
||||
iconMediaId: resolvedIconMediaId,
|
||||
videoMediaId: videoMediaId == _unchanged
|
||||
? this.videoMediaId
|
||||
: videoMediaId as String?,
|
||||
@ -957,6 +971,30 @@ List<String> _validatedImageMediaIds(List<String> ids) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
List<String> _resolvedExerciseImageMediaIds(
|
||||
String? imageMediaId,
|
||||
List<String> imageMediaIds,
|
||||
) {
|
||||
return _validatedImageMediaIds(
|
||||
imageMediaIds.isEmpty && imageMediaId != null
|
||||
? [imageMediaId]
|
||||
: imageMediaIds,
|
||||
);
|
||||
}
|
||||
|
||||
String? _validatedIconMediaId(String? iconMediaId, List<String> imageMediaIds) {
|
||||
if (iconMediaId == null) {
|
||||
return null;
|
||||
}
|
||||
final normalized = _nonBlank(iconMediaId, 'Icon media id');
|
||||
if (!imageMediaIds.contains(normalized)) {
|
||||
throw const DomainException(
|
||||
'Exercise icon must be one of the exercise images.',
|
||||
);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
void _requireAtLeastOneMeasure({
|
||||
required bool hasTime,
|
||||
required bool hasReps,
|
||||
|
||||
@ -37,7 +37,7 @@ final class AppDatabase extends _$AppDatabase {
|
||||
}
|
||||
|
||||
@override
|
||||
int get schemaVersion => 6;
|
||||
int get schemaVersion => 7;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration {
|
||||
@ -70,6 +70,9 @@ final class AppDatabase extends _$AppDatabase {
|
||||
if (from < 6) {
|
||||
await _migrateToSchema6();
|
||||
}
|
||||
if (from < 7) {
|
||||
await _migrateToSchema7();
|
||||
}
|
||||
await _createIndexes();
|
||||
},
|
||||
beforeOpen: (details) async {
|
||||
@ -260,4 +263,11 @@ extension on AppDatabase {
|
||||
'exercise_image_media_ids_snapshot_json TEXT',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _migrateToSchema7() async {
|
||||
await customStatement(
|
||||
'ALTER TABLE exercises ADD COLUMN icon_media_id TEXT '
|
||||
'REFERENCES media_assets(id)',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7959,6 +7959,20 @@ class $ExercisesTable extends Exercises
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
static const VerificationMeta _iconMediaIdMeta = const VerificationMeta(
|
||||
'iconMediaId',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<String> iconMediaId = GeneratedColumn<String>(
|
||||
'icon_media_id',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
||||
'REFERENCES media_assets (id)',
|
||||
),
|
||||
);
|
||||
static const VerificationMeta _videoMediaIdMeta = const VerificationMeta(
|
||||
'videoMediaId',
|
||||
);
|
||||
@ -8119,6 +8133,7 @@ class $ExercisesTable extends Exercises
|
||||
remoteRevision,
|
||||
name,
|
||||
description,
|
||||
iconMediaId,
|
||||
videoMediaId,
|
||||
hasTimeMeasure,
|
||||
hasRepsMeasure,
|
||||
@ -8254,6 +8269,15 @@ class $ExercisesTable extends Exercises
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('icon_media_id')) {
|
||||
context.handle(
|
||||
_iconMediaIdMeta,
|
||||
iconMediaId.isAcceptableOrUnknown(
|
||||
data['icon_media_id']!,
|
||||
_iconMediaIdMeta,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('video_media_id')) {
|
||||
context.handle(
|
||||
_videoMediaIdMeta,
|
||||
@ -8420,6 +8444,10 @@ class $ExercisesTable extends Exercises
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}description'],
|
||||
),
|
||||
iconMediaId: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}icon_media_id'],
|
||||
),
|
||||
videoMediaId: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}video_media_id'],
|
||||
@ -8491,6 +8519,7 @@ class Exercise extends DataClass implements Insertable<Exercise> {
|
||||
final String? remoteRevision;
|
||||
final String name;
|
||||
final String? description;
|
||||
final String? iconMediaId;
|
||||
final String? videoMediaId;
|
||||
final bool hasTimeMeasure;
|
||||
final bool hasRepsMeasure;
|
||||
@ -8517,6 +8546,7 @@ class Exercise extends DataClass implements Insertable<Exercise> {
|
||||
this.remoteRevision,
|
||||
required this.name,
|
||||
this.description,
|
||||
this.iconMediaId,
|
||||
this.videoMediaId,
|
||||
required this.hasTimeMeasure,
|
||||
required this.hasRepsMeasure,
|
||||
@ -8556,6 +8586,9 @@ class Exercise extends DataClass implements Insertable<Exercise> {
|
||||
if (!nullToAbsent || description != null) {
|
||||
map['description'] = Variable<String>(description);
|
||||
}
|
||||
if (!nullToAbsent || iconMediaId != null) {
|
||||
map['icon_media_id'] = Variable<String>(iconMediaId);
|
||||
}
|
||||
if (!nullToAbsent || videoMediaId != null) {
|
||||
map['video_media_id'] = Variable<String>(videoMediaId);
|
||||
}
|
||||
@ -8616,6 +8649,9 @@ class Exercise extends DataClass implements Insertable<Exercise> {
|
||||
description: description == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(description),
|
||||
iconMediaId: iconMediaId == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(iconMediaId),
|
||||
videoMediaId: videoMediaId == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(videoMediaId),
|
||||
@ -8668,6 +8704,7 @@ class Exercise extends DataClass implements Insertable<Exercise> {
|
||||
remoteRevision: serializer.fromJson<String?>(json['remoteRevision']),
|
||||
name: serializer.fromJson<String>(json['name']),
|
||||
description: serializer.fromJson<String?>(json['description']),
|
||||
iconMediaId: serializer.fromJson<String?>(json['iconMediaId']),
|
||||
videoMediaId: serializer.fromJson<String?>(json['videoMediaId']),
|
||||
hasTimeMeasure: serializer.fromJson<bool>(json['hasTimeMeasure']),
|
||||
hasRepsMeasure: serializer.fromJson<bool>(json['hasRepsMeasure']),
|
||||
@ -8705,6 +8742,7 @@ class Exercise extends DataClass implements Insertable<Exercise> {
|
||||
'remoteRevision': serializer.toJson<String?>(remoteRevision),
|
||||
'name': serializer.toJson<String>(name),
|
||||
'description': serializer.toJson<String?>(description),
|
||||
'iconMediaId': serializer.toJson<String?>(iconMediaId),
|
||||
'videoMediaId': serializer.toJson<String?>(videoMediaId),
|
||||
'hasTimeMeasure': serializer.toJson<bool>(hasTimeMeasure),
|
||||
'hasRepsMeasure': serializer.toJson<bool>(hasRepsMeasure),
|
||||
@ -8738,6 +8776,7 @@ class Exercise extends DataClass implements Insertable<Exercise> {
|
||||
Value<String?> remoteRevision = const Value.absent(),
|
||||
String? name,
|
||||
Value<String?> description = const Value.absent(),
|
||||
Value<String?> iconMediaId = const Value.absent(),
|
||||
Value<String?> videoMediaId = const Value.absent(),
|
||||
bool? hasTimeMeasure,
|
||||
bool? hasRepsMeasure,
|
||||
@ -8768,6 +8807,7 @@ class Exercise extends DataClass implements Insertable<Exercise> {
|
||||
: this.remoteRevision,
|
||||
name: name ?? this.name,
|
||||
description: description.present ? description.value : this.description,
|
||||
iconMediaId: iconMediaId.present ? iconMediaId.value : this.iconMediaId,
|
||||
videoMediaId: videoMediaId.present ? videoMediaId.value : this.videoMediaId,
|
||||
hasTimeMeasure: hasTimeMeasure ?? this.hasTimeMeasure,
|
||||
hasRepsMeasure: hasRepsMeasure ?? this.hasRepsMeasure,
|
||||
@ -8818,6 +8858,9 @@ class Exercise extends DataClass implements Insertable<Exercise> {
|
||||
description: data.description.present
|
||||
? data.description.value
|
||||
: this.description,
|
||||
iconMediaId: data.iconMediaId.present
|
||||
? data.iconMediaId.value
|
||||
: this.iconMediaId,
|
||||
videoMediaId: data.videoMediaId.present
|
||||
? data.videoMediaId.value
|
||||
: this.videoMediaId,
|
||||
@ -8871,6 +8914,7 @@ class Exercise extends DataClass implements Insertable<Exercise> {
|
||||
..write('remoteRevision: $remoteRevision, ')
|
||||
..write('name: $name, ')
|
||||
..write('description: $description, ')
|
||||
..write('iconMediaId: $iconMediaId, ')
|
||||
..write('videoMediaId: $videoMediaId, ')
|
||||
..write('hasTimeMeasure: $hasTimeMeasure, ')
|
||||
..write('hasRepsMeasure: $hasRepsMeasure, ')
|
||||
@ -8902,6 +8946,7 @@ class Exercise extends DataClass implements Insertable<Exercise> {
|
||||
remoteRevision,
|
||||
name,
|
||||
description,
|
||||
iconMediaId,
|
||||
videoMediaId,
|
||||
hasTimeMeasure,
|
||||
hasRepsMeasure,
|
||||
@ -8932,6 +8977,7 @@ class Exercise extends DataClass implements Insertable<Exercise> {
|
||||
other.remoteRevision == this.remoteRevision &&
|
||||
other.name == this.name &&
|
||||
other.description == this.description &&
|
||||
other.iconMediaId == this.iconMediaId &&
|
||||
other.videoMediaId == this.videoMediaId &&
|
||||
other.hasTimeMeasure == this.hasTimeMeasure &&
|
||||
other.hasRepsMeasure == this.hasRepsMeasure &&
|
||||
@ -8960,6 +9006,7 @@ class ExercisesCompanion extends UpdateCompanion<Exercise> {
|
||||
final Value<String?> remoteRevision;
|
||||
final Value<String> name;
|
||||
final Value<String?> description;
|
||||
final Value<String?> iconMediaId;
|
||||
final Value<String?> videoMediaId;
|
||||
final Value<bool> hasTimeMeasure;
|
||||
final Value<bool> hasRepsMeasure;
|
||||
@ -8987,6 +9034,7 @@ class ExercisesCompanion extends UpdateCompanion<Exercise> {
|
||||
this.remoteRevision = const Value.absent(),
|
||||
this.name = const Value.absent(),
|
||||
this.description = const Value.absent(),
|
||||
this.iconMediaId = const Value.absent(),
|
||||
this.videoMediaId = const Value.absent(),
|
||||
this.hasTimeMeasure = const Value.absent(),
|
||||
this.hasRepsMeasure = const Value.absent(),
|
||||
@ -9015,6 +9063,7 @@ class ExercisesCompanion extends UpdateCompanion<Exercise> {
|
||||
this.remoteRevision = const Value.absent(),
|
||||
required String name,
|
||||
this.description = const Value.absent(),
|
||||
this.iconMediaId = const Value.absent(),
|
||||
this.videoMediaId = const Value.absent(),
|
||||
required bool hasTimeMeasure,
|
||||
required bool hasRepsMeasure,
|
||||
@ -9052,6 +9101,7 @@ class ExercisesCompanion extends UpdateCompanion<Exercise> {
|
||||
Expression<String>? remoteRevision,
|
||||
Expression<String>? name,
|
||||
Expression<String>? description,
|
||||
Expression<String>? iconMediaId,
|
||||
Expression<String>? videoMediaId,
|
||||
Expression<bool>? hasTimeMeasure,
|
||||
Expression<bool>? hasRepsMeasure,
|
||||
@ -9081,6 +9131,7 @@ class ExercisesCompanion extends UpdateCompanion<Exercise> {
|
||||
if (remoteRevision != null) 'remote_revision': remoteRevision,
|
||||
if (name != null) 'name': name,
|
||||
if (description != null) 'description': description,
|
||||
if (iconMediaId != null) 'icon_media_id': iconMediaId,
|
||||
if (videoMediaId != null) 'video_media_id': videoMediaId,
|
||||
if (hasTimeMeasure != null) 'has_time_measure': hasTimeMeasure,
|
||||
if (hasRepsMeasure != null) 'has_reps_measure': hasRepsMeasure,
|
||||
@ -9114,6 +9165,7 @@ class ExercisesCompanion extends UpdateCompanion<Exercise> {
|
||||
Value<String?>? remoteRevision,
|
||||
Value<String>? name,
|
||||
Value<String?>? description,
|
||||
Value<String?>? iconMediaId,
|
||||
Value<String?>? videoMediaId,
|
||||
Value<bool>? hasTimeMeasure,
|
||||
Value<bool>? hasRepsMeasure,
|
||||
@ -9142,6 +9194,7 @@ class ExercisesCompanion extends UpdateCompanion<Exercise> {
|
||||
remoteRevision: remoteRevision ?? this.remoteRevision,
|
||||
name: name ?? this.name,
|
||||
description: description ?? this.description,
|
||||
iconMediaId: iconMediaId ?? this.iconMediaId,
|
||||
videoMediaId: videoMediaId ?? this.videoMediaId,
|
||||
hasTimeMeasure: hasTimeMeasure ?? this.hasTimeMeasure,
|
||||
hasRepsMeasure: hasRepsMeasure ?? this.hasRepsMeasure,
|
||||
@ -9204,6 +9257,9 @@ class ExercisesCompanion extends UpdateCompanion<Exercise> {
|
||||
if (description.present) {
|
||||
map['description'] = Variable<String>(description.value);
|
||||
}
|
||||
if (iconMediaId.present) {
|
||||
map['icon_media_id'] = Variable<String>(iconMediaId.value);
|
||||
}
|
||||
if (videoMediaId.present) {
|
||||
map['video_media_id'] = Variable<String>(videoMediaId.value);
|
||||
}
|
||||
@ -9266,6 +9322,7 @@ class ExercisesCompanion extends UpdateCompanion<Exercise> {
|
||||
..write('remoteRevision: $remoteRevision, ')
|
||||
..write('name: $name, ')
|
||||
..write('description: $description, ')
|
||||
..write('iconMediaId: $iconMediaId, ')
|
||||
..write('videoMediaId: $videoMediaId, ')
|
||||
..write('hasTimeMeasure: $hasTimeMeasure, ')
|
||||
..write('hasRepsMeasure: $hasRepsMeasure, ')
|
||||
@ -22861,6 +22918,27 @@ final class $$MediaAssetsTableReferences
|
||||
extends BaseReferences<_$AppDatabase, $MediaAssetsTable, MediaAsset> {
|
||||
$$MediaAssetsTableReferences(super.$_db, super.$_table, super.$_typedResult);
|
||||
|
||||
static MultiTypedResultKey<$ExercisesTable, List<Exercise>>
|
||||
_exerciseIconReferencesTable(_$AppDatabase db) =>
|
||||
MultiTypedResultKey.fromTable(
|
||||
db.exercises,
|
||||
aliasName: 'media_assets__id__exercises__icon_media_id',
|
||||
);
|
||||
|
||||
$$ExercisesTableProcessedTableManager get exerciseIconReferences {
|
||||
final manager = $$ExercisesTableTableManager(
|
||||
$_db,
|
||||
$_db.exercises,
|
||||
).filter((f) => f.iconMediaId.id.sqlEquals($_itemColumn<String>('id')!));
|
||||
|
||||
final cache = $_typedResult.readTableOrNull(
|
||||
_exerciseIconReferencesTable($_db),
|
||||
);
|
||||
return ProcessedTableManager(
|
||||
manager.$state.copyWith(prefetchedData: cache),
|
||||
);
|
||||
}
|
||||
|
||||
static MultiTypedResultKey<$ExercisesTable, List<Exercise>>
|
||||
_exerciseVideoReferencesTable(_$AppDatabase db) =>
|
||||
MultiTypedResultKey.fromTable(
|
||||
@ -23067,6 +23145,31 @@ class $$MediaAssetsTableFilterComposer
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
Expression<bool> exerciseIconReferences(
|
||||
Expression<bool> Function($$ExercisesTableFilterComposer f) f,
|
||||
) {
|
||||
final $$ExercisesTableFilterComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.id,
|
||||
referencedTable: $db.exercises,
|
||||
getReferencedColumn: (t) => t.iconMediaId,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => $$ExercisesTableFilterComposer(
|
||||
$db: $db,
|
||||
$table: $db.exercises,
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
),
|
||||
);
|
||||
return f(composer);
|
||||
}
|
||||
|
||||
Expression<bool> exerciseVideoReferences(
|
||||
Expression<bool> Function($$ExercisesTableFilterComposer f) f,
|
||||
) {
|
||||
@ -23371,6 +23474,31 @@ class $$MediaAssetsTableAnnotationComposer
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
Expression<T> exerciseIconReferences<T extends Object>(
|
||||
Expression<T> Function($$ExercisesTableAnnotationComposer a) f,
|
||||
) {
|
||||
final $$ExercisesTableAnnotationComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.id,
|
||||
referencedTable: $db.exercises,
|
||||
getReferencedColumn: (t) => t.iconMediaId,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => $$ExercisesTableAnnotationComposer(
|
||||
$db: $db,
|
||||
$table: $db.exercises,
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
),
|
||||
);
|
||||
return f(composer);
|
||||
}
|
||||
|
||||
Expression<T> exerciseVideoReferences<T extends Object>(
|
||||
Expression<T> Function($$ExercisesTableAnnotationComposer a) f,
|
||||
) {
|
||||
@ -23486,6 +23614,7 @@ class $$MediaAssetsTableTableManager
|
||||
(MediaAsset, $$MediaAssetsTableReferences),
|
||||
MediaAsset,
|
||||
PrefetchHooks Function({
|
||||
bool exerciseIconReferences,
|
||||
bool exerciseVideoReferences,
|
||||
bool exerciseImagesRefs,
|
||||
bool programExerciseImageSnapshotReferences,
|
||||
@ -23609,6 +23738,7 @@ class $$MediaAssetsTableTableManager
|
||||
.toList(),
|
||||
prefetchHooksCallback:
|
||||
({
|
||||
exerciseIconReferences = false,
|
||||
exerciseVideoReferences = false,
|
||||
exerciseImagesRefs = false,
|
||||
programExerciseImageSnapshotReferences = false,
|
||||
@ -23617,6 +23747,7 @@ class $$MediaAssetsTableTableManager
|
||||
return PrefetchHooks(
|
||||
db: db,
|
||||
explicitlyWatchedTables: [
|
||||
if (exerciseIconReferences) db.exercises,
|
||||
if (exerciseVideoReferences) db.exercises,
|
||||
if (exerciseImagesRefs) db.exerciseImages,
|
||||
if (programExerciseImageSnapshotReferences)
|
||||
@ -23627,6 +23758,27 @@ class $$MediaAssetsTableTableManager
|
||||
addJoins: null,
|
||||
getPrefetchedDataCallback: (items) async {
|
||||
return [
|
||||
if (exerciseIconReferences)
|
||||
await $_getPrefetchedData<
|
||||
MediaAsset,
|
||||
$MediaAssetsTable,
|
||||
Exercise
|
||||
>(
|
||||
currentTable: table,
|
||||
referencedTable: $$MediaAssetsTableReferences
|
||||
._exerciseIconReferencesTable(db),
|
||||
managerFromTypedResult: (p0) =>
|
||||
$$MediaAssetsTableReferences(
|
||||
db,
|
||||
table,
|
||||
p0,
|
||||
).exerciseIconReferences,
|
||||
referencedItemsForCurrentItem:
|
||||
(item, referencedItems) => referencedItems.where(
|
||||
(e) => e.iconMediaId == item.id,
|
||||
),
|
||||
typedResults: items,
|
||||
),
|
||||
if (exerciseVideoReferences)
|
||||
await $_getPrefetchedData<
|
||||
MediaAsset,
|
||||
@ -23734,6 +23886,7 @@ typedef $$MediaAssetsTableProcessedTableManager =
|
||||
(MediaAsset, $$MediaAssetsTableReferences),
|
||||
MediaAsset,
|
||||
PrefetchHooks Function({
|
||||
bool exerciseIconReferences,
|
||||
bool exerciseVideoReferences,
|
||||
bool exerciseImagesRefs,
|
||||
bool programExerciseImageSnapshotReferences,
|
||||
@ -23755,6 +23908,7 @@ typedef $$ExercisesTableCreateCompanionBuilder =
|
||||
Value<String?> remoteRevision,
|
||||
required String name,
|
||||
Value<String?> description,
|
||||
Value<String?> iconMediaId,
|
||||
Value<String?> videoMediaId,
|
||||
required bool hasTimeMeasure,
|
||||
required bool hasRepsMeasure,
|
||||
@ -23784,6 +23938,7 @@ typedef $$ExercisesTableUpdateCompanionBuilder =
|
||||
Value<String?> remoteRevision,
|
||||
Value<String> name,
|
||||
Value<String?> description,
|
||||
Value<String?> iconMediaId,
|
||||
Value<String?> videoMediaId,
|
||||
Value<bool> hasTimeMeasure,
|
||||
Value<bool> hasRepsMeasure,
|
||||
@ -23803,6 +23958,23 @@ final class $$ExercisesTableReferences
|
||||
extends BaseReferences<_$AppDatabase, $ExercisesTable, Exercise> {
|
||||
$$ExercisesTableReferences(super.$_db, super.$_table, super.$_typedResult);
|
||||
|
||||
static $MediaAssetsTable _iconMediaIdTable(_$AppDatabase db) =>
|
||||
db.mediaAssets.createAlias('exercises__icon_media_id__media_assets__id');
|
||||
|
||||
$$MediaAssetsTableProcessedTableManager? get iconMediaId {
|
||||
final $_column = $_itemColumn<String>('icon_media_id');
|
||||
if ($_column == null) return null;
|
||||
final manager = $$MediaAssetsTableTableManager(
|
||||
$_db,
|
||||
$_db.mediaAssets,
|
||||
).filter((f) => f.id.sqlEquals($_column));
|
||||
final item = $_typedResult.readTableOrNull(_iconMediaIdTable($_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');
|
||||
|
||||
@ -23988,6 +24160,29 @@ class $$ExercisesTableFilterComposer
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
$$MediaAssetsTableFilterComposer get iconMediaId {
|
||||
final $$MediaAssetsTableFilterComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.iconMediaId,
|
||||
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,
|
||||
@ -24191,6 +24386,29 @@ class $$ExercisesTableOrderingComposer
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
$$MediaAssetsTableOrderingComposer get iconMediaId {
|
||||
final $$MediaAssetsTableOrderingComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.iconMediaId,
|
||||
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,
|
||||
@ -24330,6 +24548,29 @@ class $$ExercisesTableAnnotationComposer
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
$$MediaAssetsTableAnnotationComposer get iconMediaId {
|
||||
final $$MediaAssetsTableAnnotationComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.iconMediaId,
|
||||
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,
|
||||
@ -24418,6 +24659,7 @@ class $$ExercisesTableTableManager
|
||||
(Exercise, $$ExercisesTableReferences),
|
||||
Exercise,
|
||||
PrefetchHooks Function({
|
||||
bool iconMediaId,
|
||||
bool videoMediaId,
|
||||
bool exerciseImagesRefs,
|
||||
bool programExercisesRefs,
|
||||
@ -24449,6 +24691,7 @@ class $$ExercisesTableTableManager
|
||||
Value<String?> remoteRevision = const Value.absent(),
|
||||
Value<String> name = const Value.absent(),
|
||||
Value<String?> description = const Value.absent(),
|
||||
Value<String?> iconMediaId = const Value.absent(),
|
||||
Value<String?> videoMediaId = const Value.absent(),
|
||||
Value<bool> hasTimeMeasure = const Value.absent(),
|
||||
Value<bool> hasRepsMeasure = const Value.absent(),
|
||||
@ -24476,6 +24719,7 @@ class $$ExercisesTableTableManager
|
||||
remoteRevision: remoteRevision,
|
||||
name: name,
|
||||
description: description,
|
||||
iconMediaId: iconMediaId,
|
||||
videoMediaId: videoMediaId,
|
||||
hasTimeMeasure: hasTimeMeasure,
|
||||
hasRepsMeasure: hasRepsMeasure,
|
||||
@ -24505,6 +24749,7 @@ class $$ExercisesTableTableManager
|
||||
Value<String?> remoteRevision = const Value.absent(),
|
||||
required String name,
|
||||
Value<String?> description = const Value.absent(),
|
||||
Value<String?> iconMediaId = const Value.absent(),
|
||||
Value<String?> videoMediaId = const Value.absent(),
|
||||
required bool hasTimeMeasure,
|
||||
required bool hasRepsMeasure,
|
||||
@ -24532,6 +24777,7 @@ class $$ExercisesTableTableManager
|
||||
remoteRevision: remoteRevision,
|
||||
name: name,
|
||||
description: description,
|
||||
iconMediaId: iconMediaId,
|
||||
videoMediaId: videoMediaId,
|
||||
hasTimeMeasure: hasTimeMeasure,
|
||||
hasRepsMeasure: hasRepsMeasure,
|
||||
@ -24556,6 +24802,7 @@ class $$ExercisesTableTableManager
|
||||
.toList(),
|
||||
prefetchHooksCallback:
|
||||
({
|
||||
iconMediaId = false,
|
||||
videoMediaId = false,
|
||||
exerciseImagesRefs = false,
|
||||
programExercisesRefs = false,
|
||||
@ -24582,6 +24829,19 @@ class $$ExercisesTableTableManager
|
||||
dynamic
|
||||
>
|
||||
>(state) {
|
||||
if (iconMediaId) {
|
||||
state =
|
||||
state.withJoin(
|
||||
currentTable: table,
|
||||
currentColumn: table.iconMediaId,
|
||||
referencedTable: $$ExercisesTableReferences
|
||||
._iconMediaIdTable(db),
|
||||
referencedColumn: $$ExercisesTableReferences
|
||||
._iconMediaIdTable(db)
|
||||
.id,
|
||||
)
|
||||
as T;
|
||||
}
|
||||
if (videoMediaId) {
|
||||
state =
|
||||
state.withJoin(
|
||||
@ -24663,6 +24923,7 @@ typedef $$ExercisesTableProcessedTableManager =
|
||||
(Exercise, $$ExercisesTableReferences),
|
||||
Exercise,
|
||||
PrefetchHooks Function({
|
||||
bool iconMediaId,
|
||||
bool videoMediaId,
|
||||
bool exerciseImagesRefs,
|
||||
bool programExercisesRefs,
|
||||
|
||||
@ -1078,6 +1078,7 @@ db.ExercisesCompanion _exerciseCompanion(domain.Exercise exercise) {
|
||||
remoteRevision: values[10] as Value<String?>,
|
||||
name: Value(exercise.name),
|
||||
description: Value(exercise.description),
|
||||
iconMediaId: Value(exercise.iconMediaId),
|
||||
videoMediaId: Value(exercise.videoMediaId),
|
||||
hasTimeMeasure: Value(exercise.hasTimeMeasure),
|
||||
hasRepsMeasure: Value(exercise.hasRepsMeasure),
|
||||
@ -1099,6 +1100,7 @@ domain.Exercise _exerciseFromRow(db.Exercise row, List<String> imageMediaIds) {
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
imageMediaIds: imageMediaIds,
|
||||
iconMediaId: row.iconMediaId,
|
||||
videoMediaId: row.videoMediaId,
|
||||
hasTimeMeasure: row.hasTimeMeasure,
|
||||
hasRepsMeasure: row.hasRepsMeasure,
|
||||
|
||||
@ -55,6 +55,9 @@ class Exercises extends SyncableTable {
|
||||
|
||||
TextColumn get name => text().withLength(min: 1)();
|
||||
TextColumn get description => text().nullable()();
|
||||
@ReferenceName('exerciseIconReferences')
|
||||
TextColumn get iconMediaId =>
|
||||
text().nullable().references(MediaAssets, #id)();
|
||||
@ReferenceName('exerciseVideoReferences')
|
||||
TextColumn get videoMediaId =>
|
||||
text().nullable().references(MediaAssets, #id)();
|
||||
|
||||
@ -267,15 +267,11 @@ final class ExerciseListTile extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final description = exercise.description?.trim();
|
||||
final hasMedia =
|
||||
exercise.imageMediaId != null || exercise.videoMediaId != null;
|
||||
final iconMediaId = exercise.iconMediaId ?? exercise.imageMediaId;
|
||||
final hasMedia = iconMediaId != null || exercise.videoMediaId != null;
|
||||
return ListTile(
|
||||
onTap: onTap,
|
||||
leading: CircleAvatar(
|
||||
child: Icon(
|
||||
hasMedia ? Icons.perm_media_outlined : Icons.fitness_center,
|
||||
),
|
||||
),
|
||||
leading: _ExerciseIconAvatar(iconMediaId: iconMediaId),
|
||||
title: Text(exercise.name),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -328,6 +324,42 @@ final class ExerciseListTile extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
final class _ExerciseIconAvatar extends StatelessWidget {
|
||||
const _ExerciseIconAvatar({required this.iconMediaId});
|
||||
|
||||
final String? iconMediaId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final icon = CircleAvatar(
|
||||
child: Icon(
|
||||
iconMediaId == null ? Icons.fitness_center : Icons.perm_media_outlined,
|
||||
),
|
||||
);
|
||||
if (iconMediaId == null) {
|
||||
return icon;
|
||||
}
|
||||
return Tooltip(
|
||||
message: 'Icône d’exercice $iconMediaId',
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
icon,
|
||||
Positioned(
|
||||
right: -2,
|
||||
bottom: -2,
|
||||
child: Icon(
|
||||
Icons.star,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class ExerciseFormScreen extends StatefulWidget {
|
||||
const ExerciseFormScreen({
|
||||
required this.exerciseUseCases,
|
||||
@ -357,6 +389,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
late final TextEditingController _defaultScoreController;
|
||||
late final TextEditingController _defaultScoreTimeController;
|
||||
late List<String> _imageMediaIds;
|
||||
String? _iconMediaId;
|
||||
String? _videoMediaId;
|
||||
final _selectedImageNamesById = <String, String>{};
|
||||
String? _selectedVideoName;
|
||||
@ -395,6 +428,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
),
|
||||
);
|
||||
_imageMediaIds = List<String>.of(exercise?.imageMediaIds ?? const []);
|
||||
_iconMediaId = exercise?.iconMediaId;
|
||||
_videoMediaId = exercise?.videoMediaId;
|
||||
_hasTime = exercise?.hasTimeMeasure ?? true;
|
||||
_hasReps = exercise?.hasRepsMeasure ?? false;
|
||||
@ -450,9 +484,11 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
_ImageGalleryField(
|
||||
imageMediaIds: _imageMediaIds,
|
||||
imageNamesById: _selectedImageNamesById,
|
||||
iconMediaId: _iconMediaId,
|
||||
importing: _importingImage,
|
||||
onAdd: _importImage,
|
||||
onRemove: _removeImage,
|
||||
onSetIcon: _setIcon,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_MediaImportField(
|
||||
@ -704,10 +740,17 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
_imageMediaIds = _imageMediaIds
|
||||
.where((imageId) => imageId != mediaAssetId)
|
||||
.toList();
|
||||
if (_iconMediaId == mediaAssetId) {
|
||||
_iconMediaId = null;
|
||||
}
|
||||
_selectedImageNamesById.remove(mediaAssetId);
|
||||
});
|
||||
}
|
||||
|
||||
void _setIcon(String mediaAssetId) {
|
||||
setState(() => _iconMediaId = mediaAssetId);
|
||||
}
|
||||
|
||||
Future<void> _importVideo() async {
|
||||
final sourcePath = await widget.mediaPicker.pickPath(MediaKind.video);
|
||||
if (sourcePath == null) {
|
||||
@ -795,6 +838,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
name: _nameController.text.trim(),
|
||||
description: _optionalText(_descriptionController),
|
||||
imageMediaIds: _imageMediaIds,
|
||||
iconMediaId: _iconMediaId,
|
||||
videoMediaId: _videoMediaId,
|
||||
hasTimeMeasure: _hasTime,
|
||||
hasRepsMeasure: _hasReps,
|
||||
@ -813,6 +857,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
name: _nameController.text.trim(),
|
||||
description: _optionalText(_descriptionController),
|
||||
imageMediaIds: _imageMediaIds,
|
||||
iconMediaId: _iconMediaId,
|
||||
videoMediaId: _videoMediaId,
|
||||
hasTimeMeasure: _hasTime,
|
||||
hasRepsMeasure: _hasReps,
|
||||
@ -969,16 +1014,20 @@ final class _ImageGalleryField extends StatelessWidget {
|
||||
const _ImageGalleryField({
|
||||
required this.imageMediaIds,
|
||||
required this.imageNamesById,
|
||||
required this.iconMediaId,
|
||||
required this.importing,
|
||||
required this.onAdd,
|
||||
required this.onRemove,
|
||||
required this.onSetIcon,
|
||||
});
|
||||
|
||||
final List<String> imageMediaIds;
|
||||
final Map<String, String> imageNamesById;
|
||||
final String? iconMediaId;
|
||||
final bool importing;
|
||||
final VoidCallback onAdd;
|
||||
final ValueChanged<String> onRemove;
|
||||
final ValueChanged<String> onSetIcon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -998,10 +1047,12 @@ final class _ImageGalleryField extends StatelessWidget {
|
||||
for (var index = 0; index < imageMediaIds.length; index++)
|
||||
_ImageThumbnail(
|
||||
mediaAssetId: imageMediaIds[index],
|
||||
selectedAsIcon: imageMediaIds[index] == iconMediaId,
|
||||
label:
|
||||
imageNamesById[imageMediaIds[index]] ??
|
||||
'Image ${index + 1}',
|
||||
onRemove: onRemove,
|
||||
onSetIcon: onSetIcon,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -1035,44 +1086,78 @@ final class _ImageGalleryField extends StatelessWidget {
|
||||
final class _ImageThumbnail extends StatelessWidget {
|
||||
const _ImageThumbnail({
|
||||
required this.mediaAssetId,
|
||||
required this.selectedAsIcon,
|
||||
required this.label,
|
||||
required this.onRemove,
|
||||
required this.onSetIcon,
|
||||
});
|
||||
|
||||
final String mediaAssetId;
|
||||
final bool selectedAsIcon;
|
||||
final String label;
|
||||
final ValueChanged<String> onRemove;
|
||||
final ValueChanged<String> onSetIcon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final tokens = courtBlazerTokensOf(context);
|
||||
return SizedBox(
|
||||
width: 104,
|
||||
width: 132,
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 104,
|
||||
height: 154,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: tokens.border),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
border: Border.all(
|
||||
color: selectedAsIcon
|
||||
? theme.colorScheme.primary
|
||||
: tokens.border,
|
||||
width: selectedAsIcon ? 2 : 1,
|
||||
),
|
||||
color: theme.colorScheme.surface,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.image_outlined),
|
||||
Icon(
|
||||
Icons.image_outlined,
|
||||
color: selectedAsIcon ? theme.colorScheme.primary : null,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
style: theme.textTheme.labelSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
IconButton(
|
||||
tooltip: 'Définir comme icône',
|
||||
onPressed: selectedAsIcon
|
||||
? null
|
||||
: () => onSetIcon(mediaAssetId),
|
||||
icon: const Icon(Icons.star_outline, size: 18),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (selectedAsIcon)
|
||||
Positioned(
|
||||
top: 4,
|
||||
left: 4,
|
||||
child: Tooltip(
|
||||
message: 'Icône choisie',
|
||||
child: Icon(
|
||||
Icons.star,
|
||||
color: theme.colorScheme.primary,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 2,
|
||||
right: 2,
|
||||
|
||||
@ -897,7 +897,8 @@ final class _ProgramExerciseDraft {
|
||||
sourceExerciseId: exercise.metadata.id,
|
||||
exerciseNameSnapshot: exercise.name,
|
||||
exerciseDescriptionSnapshot: exercise.description,
|
||||
exerciseImageMediaIdSnapshot: exercise.imageMediaId,
|
||||
exerciseImageMediaIdSnapshot:
|
||||
exercise.iconMediaId ?? exercise.imageMediaId,
|
||||
exerciseImageMediaIdsSnapshot: exercise.imageMediaIds,
|
||||
exerciseVideoMediaIdSnapshot: exercise.videoMediaId,
|
||||
exerciseArchivedSnapshot: exercise.archivedAt != null,
|
||||
|
||||
@ -225,6 +225,72 @@ void main() {
|
||||
expect(updated.imageMediaId, 'media-1');
|
||||
});
|
||||
|
||||
test('Exercise icon can be set to an image from the gallery', () async {
|
||||
final repository = _FakeExerciseRepository()
|
||||
..exercise = Exercise(
|
||||
metadata: _metadata('exercise-1'),
|
||||
name: 'Squat',
|
||||
imageMediaIds: const ['media-1', 'media-2'],
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: false,
|
||||
defaultTargetReps: 10,
|
||||
);
|
||||
final useCase = _exerciseUseCase(repository);
|
||||
|
||||
final updated = await useCase.setIcon(
|
||||
exerciseId: 'exercise-1',
|
||||
mediaAssetId: 'media-2',
|
||||
);
|
||||
|
||||
expect(updated.iconMediaId, 'media-2');
|
||||
expect(repository.saved.single.iconMediaId, 'media-2');
|
||||
});
|
||||
|
||||
test('Exercise icon rejects images outside the gallery', () async {
|
||||
final repository = _FakeExerciseRepository()
|
||||
..exercise = Exercise(
|
||||
metadata: _metadata('exercise-1'),
|
||||
name: 'Squat',
|
||||
imageMediaIds: const ['media-1'],
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: false,
|
||||
defaultTargetReps: 10,
|
||||
);
|
||||
final useCase = _exerciseUseCase(repository);
|
||||
|
||||
await expectLater(
|
||||
useCase.setIcon(exerciseId: 'exercise-1', mediaAssetId: 'media-2'),
|
||||
throwsA(isA<DomainException>()),
|
||||
);
|
||||
expect(repository.saved, isEmpty);
|
||||
});
|
||||
|
||||
test('Exercise icon is reset when its image is removed', () async {
|
||||
final repository = _FakeExerciseRepository()
|
||||
..exercise = Exercise(
|
||||
metadata: _metadata('exercise-1'),
|
||||
name: 'Squat',
|
||||
imageMediaIds: const ['media-1', 'media-2'],
|
||||
iconMediaId: 'media-2',
|
||||
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']);
|
||||
expect(updated.iconMediaId, isNull);
|
||||
expect(repository.saved.single.iconMediaId, isNull);
|
||||
});
|
||||
|
||||
test('program exercise snapshot keeps the full image gallery', () async {
|
||||
final exerciseRepository = _FakeExerciseRepository()
|
||||
..exercise = Exercise(
|
||||
|
||||
@ -216,6 +216,72 @@ void main() {
|
||||
expect(find.text('Aucune image sélectionnée'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('désigner une image comme icône l’indique visuellement', (
|
||||
tester,
|
||||
) async {
|
||||
final exerciseRepository = _FakeExerciseRepository();
|
||||
final storage = _FakeMediaStorage();
|
||||
|
||||
await tester.binding.setSurfaceSize(const Size(400, 1600));
|
||||
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',
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Squat');
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextFormField, 'Temps par défaut (s)'),
|
||||
'45',
|
||||
);
|
||||
await tester.tap(find.text('Ajouter une image'));
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Ajouter une image'));
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byTooltip('Définir comme icône').last);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byTooltip('Icône choisie'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Enregistrer'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(exerciseRepository.saved.single.iconMediaId, 'id-2');
|
||||
});
|
||||
|
||||
testWidgets('ExerciseListTile utilise l’icône choisie', (tester) async {
|
||||
final exercise = Exercise(
|
||||
metadata: _metadata('exercise-1'),
|
||||
name: 'Squat',
|
||||
imageMediaIds: const ['image-1', 'image-2'],
|
||||
iconMediaId: 'image-2',
|
||||
hasTimeMeasure: true,
|
||||
hasRepsMeasure: false,
|
||||
hasScoreMeasure: false,
|
||||
defaultTargetTimeSeconds: 45,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(body: ExerciseListTile(exercise: exercise)),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byTooltip('Icône d’exercice image-2'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'supprimer un exercice demande confirmation puis le retire de la liste',
|
||||
(tester) async {
|
||||
|
||||
Reference in New Issue
Block a user