merge(main): icône d'exercice (ticket #38)
Fusionne feature/#38-icone-exercice — analyze propre, 72/72 tests verts, build APK debug validé. Dernier ticket du backlog, clôturé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,6 +1,9 @@
|
||||
---
|
||||
issueRef: "#38"
|
||||
version: 3
|
||||
updatedBy: {"kind":"user"}
|
||||
updatedAt: 1784330204091
|
||||
version: 6
|
||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||
updatedAt: 1784377509677
|
||||
---
|
||||
Découpage :
|
||||
1. [DevBackend] Ajouter un champ `iconMediaId` (String?, nullable) sur Exercise, distinct de la galerie `imageMediaIds` (ticket #35) — l'icône doit référencer l'un des médias déjà présents dans la galerie de l'exercice (contrainte : iconMediaId, si renseigné, doit faire partie de imageMediaIds). Si iconMediaId est null, l'affichage de la liste retombe sur la première image de la galerie (comportement actuel) ou aucune icône si la galerie est vide. Migration Drift.
|
||||
2. [DevFrontend] Dans le formulaire d'exercice, une fois qu'il y a au moins une image dans la galerie, permettre de désigner l'une d'elles comme icône (ex: tap longue ou bouton "Définir comme icône" sur chaque miniature, avec un indicateur visuel sur celle actuellement choisie). Utiliser cette icône dans ExerciseListTile et partout où l'exercice est affiché en liste compacte (programme, sélection d'exercice).
|
||||
@ -2,15 +2,15 @@
|
||||
id: "51e8769d-7b2b-4b4d-8de1-dc784881e575"
|
||||
number: 38
|
||||
title: "Proposer de mettre une image pour l'icone de l'exercice"
|
||||
status: "open"
|
||||
status: "closed"
|
||||
priority: "medium"
|
||||
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: 1784330169746
|
||||
updatedAt: 1784330204091
|
||||
version: 3
|
||||
updatedAt: 1784377509677
|
||||
version: 6
|
||||
---
|
||||
J'aimerais qu'en plus des images que l'on peut ajouter, qu'il soit possible de mettre une image comme icone de l'exercice
|
||||
@ -429,13 +429,13 @@
|
||||
"issueRef": "#38",
|
||||
"path": "38",
|
||||
"title": "Proposer de mettre une image pour l'icone de l'exercice",
|
||||
"status": "open",
|
||||
"status": "closed",
|
||||
"priority": "medium",
|
||||
"sprint": null,
|
||||
"assignedAgentIds": [
|
||||
"57695b92-24d0-4876-837c-76116e70a6ae"
|
||||
],
|
||||
"updatedAt": 1784330204091
|
||||
"updatedAt": 1784377509677
|
||||
},
|
||||
{
|
||||
"issueRef": "#39",
|
||||
|
||||
@ -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