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:
@ -334,9 +334,11 @@ final class Exercise {
|
||||
this.autoStartNextTimedStep = true,
|
||||
this.category = ExerciseCategory.uncategorized,
|
||||
this.isExample = false,
|
||||
List<String> tags = const [],
|
||||
List<ExerciseStep> steps = const [],
|
||||
this.archivedAt,
|
||||
}) : name = _nonBlank(name, 'Exercise name'),
|
||||
tags = normalizeTags(tags),
|
||||
imageMediaIds = _resolvedExerciseImageMediaIds(
|
||||
imageMediaId,
|
||||
imageMediaIds,
|
||||
@ -394,6 +396,7 @@ final class Exercise {
|
||||
final bool autoStartNextTimedStep;
|
||||
final ExerciseCategory category;
|
||||
final bool isExample;
|
||||
final List<String> tags;
|
||||
final List<ExerciseStep> steps;
|
||||
final DateTime? archivedAt;
|
||||
|
||||
@ -431,6 +434,7 @@ final class Exercise {
|
||||
bool? autoStartNextTimedStep,
|
||||
ExerciseCategory? category,
|
||||
bool? isExample,
|
||||
Object? tags = _unchanged,
|
||||
Object? steps = _unchanged,
|
||||
Object? archivedAt = _unchanged,
|
||||
}) {
|
||||
@ -482,6 +486,7 @@ final class Exercise {
|
||||
autoStartNextTimedStep ?? this.autoStartNextTimedStep,
|
||||
category: category ?? this.category,
|
||||
isExample: isExample ?? this.isExample,
|
||||
tags: tags == _unchanged ? this.tags : tags as List<String>,
|
||||
steps: steps == _unchanged ? this.steps : steps as List<ExerciseStep>,
|
||||
archivedAt: archivedAt == _unchanged
|
||||
? this.archivedAt
|
||||
@ -552,8 +557,10 @@ final class Program {
|
||||
required String name,
|
||||
required this.defaultRestSeconds,
|
||||
this.isExample = false,
|
||||
List<String> tags = const [],
|
||||
this.exercises = const [],
|
||||
}) : name = _nonBlank(name, 'Program name') {
|
||||
}) : name = _nonBlank(name, 'Program name'),
|
||||
tags = normalizeTags(tags) {
|
||||
_requireNonNegative(defaultRestSeconds, 'Default rest seconds');
|
||||
}
|
||||
|
||||
@ -561,6 +568,7 @@ final class Program {
|
||||
final String name;
|
||||
final int defaultRestSeconds;
|
||||
final bool isExample;
|
||||
final List<String> tags;
|
||||
final List<ProgramExercise> exercises;
|
||||
|
||||
Program copyWith({
|
||||
@ -569,12 +577,14 @@ final class Program {
|
||||
String? name,
|
||||
int? defaultRestSeconds,
|
||||
bool? isExample,
|
||||
Object? tags = _unchanged,
|
||||
}) {
|
||||
return Program(
|
||||
metadata: metadata ?? this.metadata,
|
||||
name: name ?? this.name,
|
||||
defaultRestSeconds: defaultRestSeconds ?? this.defaultRestSeconds,
|
||||
isExample: isExample ?? this.isExample,
|
||||
tags: tags == _unchanged ? this.tags : tags as List<String>,
|
||||
exercises: exercises ?? this.exercises,
|
||||
);
|
||||
}
|
||||
@ -769,14 +779,17 @@ final class WorkoutTemplate {
|
||||
required String name,
|
||||
this.lastStartedAt,
|
||||
this.isExample = false,
|
||||
List<String> tags = const [],
|
||||
this.programs = const [],
|
||||
this.overrides = const [],
|
||||
}) : name = _nonBlank(name, 'Workout template name');
|
||||
}) : name = _nonBlank(name, 'Workout template name'),
|
||||
tags = normalizeTags(tags);
|
||||
|
||||
final EntityMetadata metadata;
|
||||
final String name;
|
||||
final DateTime? lastStartedAt;
|
||||
final bool isExample;
|
||||
final List<String> tags;
|
||||
final List<WorkoutTemplateProgram> programs;
|
||||
final List<WorkoutTemplateExerciseOverride> overrides;
|
||||
}
|
||||
@ -1591,6 +1604,28 @@ String _nonBlank(String? value, String label) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
List<String> normalizeTags(Iterable<String> tags) {
|
||||
final normalized = <String>[];
|
||||
final seen = <String>{};
|
||||
for (final tag in tags) {
|
||||
final value = tag.trim().replaceAll(RegExp(r'\s+'), ' ').toLowerCase();
|
||||
if (value.isEmpty) {
|
||||
throw const DomainException('Tag must not be blank.');
|
||||
}
|
||||
if (value.length > 24) {
|
||||
throw const DomainException('Tag must be 24 characters or less.');
|
||||
}
|
||||
if (!seen.add(value)) {
|
||||
throw const DomainException('Tags must be unique.');
|
||||
}
|
||||
normalized.add(value);
|
||||
}
|
||||
if (normalized.length > 8) {
|
||||
throw const DomainException('An item cannot have more than 8 tags.');
|
||||
}
|
||||
return List.unmodifiable(normalized);
|
||||
}
|
||||
|
||||
List<String> _validatedImageMediaIds(List<String> ids) {
|
||||
if (ids.length > 5) {
|
||||
throw const DomainException('An exercise cannot have more than 5 images.');
|
||||
|
||||
Reference in New Issue
Block a user