feat(bibliotheque): tags et duplication profonde

Ticket #83 B1-B3 : tags JSON dénormalisés, migration Drift v19, mappings sync, helpers de filtre/suggestions, duplication profonde Program/WorkoutTemplate, tests associés.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 10:01:31 +02:00
parent 10fba8d2ec
commit add5025dd2
9 changed files with 1012 additions and 10 deletions

View File

@ -2816,6 +2816,7 @@ db.ExercisesCompanion _exerciseCompanion(domain.Exercise exercise) {
defaultTargetScore: Value(exercise.defaultTargetScore),
defaultTargetScoreTimeMs: Value(exercise.defaultTargetScoreTimeMs),
autoStartNextTimedStep: Value(exercise.autoStartNextTimedStep),
tagsJson: Value(_encodeTags(exercise.tags)),
archivedAt: Value(_utcOrNull(exercise.archivedAt)),
);
}
@ -2846,6 +2847,7 @@ domain.Exercise _exerciseFromRow(
autoStartNextTimedStep: row.autoStartNextTimedStep,
category: starterMetadata.category,
isExample: starterMetadata.isExample,
tags: _decodeTags(row.tagsJson),
steps: steps,
archivedAt: _utcOrNull(row.archivedAt),
);
@ -3028,6 +3030,7 @@ db.ProgramsCompanion _programCompanion(domain.Program program) {
remoteRevision: values[10] as Value<String?>,
name: Value(program.name),
defaultRestSeconds: Value(program.defaultRestSeconds),
tagsJson: Value(_encodeTags(program.tags)),
);
}
@ -3041,6 +3044,7 @@ domain.Program _programFromRow(
name: row.name,
defaultRestSeconds: row.defaultRestSeconds,
isExample: isExample,
tags: _decodeTags(row.tagsJson),
exercises: exercises,
);
}
@ -3157,6 +3161,7 @@ db.WorkoutTemplatesCompanion _workoutTemplateCompanion(
remoteRevision: values[10] as Value<String?>,
name: Value(template.name),
lastStartedAt: Value(_utcOrNull(template.lastStartedAt)),
tagsJson: Value(_encodeTags(template.tags)),
);
}
@ -3171,6 +3176,7 @@ domain.WorkoutTemplate _workoutTemplateFromRow(
name: row.name,
lastStartedAt: _utcOrNull(row.lastStartedAt),
isExample: isExample,
tags: _decodeTags(row.tagsJson),
programs: programs,
overrides: overrides,
);
@ -4077,6 +4083,7 @@ Map<String, Object?> _exercisePayload(domain.Exercise exercise) => {
'autoStartNextTimedStep': exercise.autoStartNextTimedStep,
'category': exercise.category.name,
'isExample': exercise.isExample,
'tags': exercise.tags,
'steps': exercise.steps.map((step) => step.toSnapshotJson()).toList(),
'archivedAt': exercise.archivedAt?.toUtc().toIso8601String(),
};
@ -4102,6 +4109,7 @@ Map<String, Object?> _programPayload(domain.Program program) => {
'name': program.name,
'defaultRestSeconds': program.defaultRestSeconds,
'isExample': program.isExample,
'tags': program.tags,
'exercises': program.exercises
.map((exercise) => exercise.toSnapshotJson())
.toList(),
@ -4114,6 +4122,7 @@ Map<String, Object?> _workoutTemplatePayload(domain.WorkoutTemplate template) =>
'name': template.name,
'lastStartedAt': template.lastStartedAt?.toUtc().toIso8601String(),
'isExample': template.isExample,
'tags': template.tags,
'programs': template.programs
.map(
(program) => {
@ -4183,6 +4192,7 @@ domain.Exercise _exerciseFromPayload(RemoteSyncedItem item) {
payload['category'] as String? ?? 'uncategorized',
),
isExample: payload['isExample'] as bool? ?? false,
tags: _stringListFromPayload(payload['tags']),
steps: _stepsFromPayload(payload['steps']),
archivedAt: _dateTimeFromPayload(payload['archivedAt']),
);
@ -4213,6 +4223,7 @@ domain.Program _programFromPayload(RemoteSyncedItem item) {
name: _stringFromPayload(payload, 'name', item.clientId),
defaultRestSeconds: payload['defaultRestSeconds'] as int? ?? 0,
isExample: payload['isExample'] as bool? ?? false,
tags: _stringListFromPayload(payload['tags']),
exercises: _programExercisesFromPayload(payload['exercises'], metadata),
);
}
@ -4225,6 +4236,7 @@ domain.WorkoutTemplate _workoutTemplateFromPayload(RemoteSyncedItem item) {
name: _stringFromPayload(payload, 'name', item.clientId),
lastStartedAt: _dateTimeFromPayload(payload['lastStartedAt']),
isExample: payload['isExample'] as bool? ?? false,
tags: _stringListFromPayload(payload['tags']),
programs: _workoutTemplateProgramsFromPayload(
payload['programs'],
metadata,
@ -4422,6 +4434,23 @@ List<String> _stringListFromPayload(Object? value) {
return value.whereType<String>().toList(growable: false);
}
String _encodeTags(List<String> tags) => jsonEncode(domain.normalizeTags(tags));
List<String> _decodeTags(String? encoded) {
if (encoded == null || encoded.trim().isEmpty) {
return const [];
}
try {
final decoded = jsonDecode(encoded);
if (decoded is! List) {
return const [];
}
return domain.normalizeTags(decoded.whereType<String>());
} on Object {
return const [];
}
}
List<domain.ExerciseStep> _stepsFromPayload(Object? value) {
if (value is! List) {
return const [];