feat(ui): écran Composition de séance-modèle

Ajoute l'écran de composition de séance-modèle
(presentation/workout_template_screen.dart), avec les ajustements
application/domaine/repositories nécessaires et la navigation depuis
l'accueil. flutter analyze propre, 12/12 tests verts, build APK debug
validé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 18:59:59 +02:00
parent be02bbfeea
commit 89d89a3005
9 changed files with 1029 additions and 7 deletions

View File

@ -7,12 +7,14 @@ final class AppBootstrap {
required this.exerciseUseCases,
required this.mediaUseCases,
required this.programUseCases,
required this.workoutTemplateUseCases,
});
final AppDatabase database;
final ExerciseUseCases exerciseUseCases;
final MediaUseCases mediaUseCases;
final ProgramUseCases programUseCases;
final WorkoutTemplateUseCases workoutTemplateUseCases;
static Future<AppBootstrap> create() async {
final database = AppDatabase.open();
@ -20,6 +22,7 @@ final class AppBootstrap {
final exerciseRepository = DriftExerciseRepository(database);
final mediaRepository = DriftMediaAssetRepository(database);
final programRepository = DriftProgramRepository(database);
final templateRepository = DriftWorkoutTemplateRepository(database);
final ids = LocalIdGenerator();
const clock = SystemClock();
const originDeviceId = 'local-device';
@ -47,6 +50,13 @@ final class AppBootstrap {
ids: ids,
originDeviceId: originDeviceId,
),
workoutTemplateUseCases: WorkoutTemplateUseCases(
templateRepository: templateRepository,
programRepository: programRepository,
clock: clock,
ids: ids,
originDeviceId: originDeviceId,
),
);
}

View File

@ -67,9 +67,11 @@ abstract interface class ProgramRepository {
abstract interface class WorkoutTemplateRepository {
Future<WorkoutTemplate?> findById(String id);
Future<List<WorkoutTemplate>> listActive();
Future<void> save(WorkoutTemplate template);
Future<void> saveProgram(WorkoutTemplateProgram program);
Future<void> saveOverride(WorkoutTemplateExerciseOverride override);
Future<void> replaceComposition(WorkoutTemplate template, DateTime deletedAt);
}
abstract interface class ActiveSessionRepository {

View File

@ -398,6 +398,14 @@ final class WorkoutTemplateUseCases {
final IdGenerator ids;
final String originDeviceId;
Future<WorkoutTemplate?> findById(String id) {
return templateRepository.findById(id);
}
Future<List<WorkoutTemplate>> listActive() {
return templateRepository.listActive();
}
Future<WorkoutTemplate> create({required String name}) async {
final now = clock.now();
final template = WorkoutTemplate(
@ -408,6 +416,78 @@ final class WorkoutTemplateUseCases {
return template;
}
Future<WorkoutTemplate> saveConfigured({
String? id,
required String name,
required List<WorkoutTemplateProgramConfig> programs,
required List<WorkoutTemplateExerciseOverrideConfig> overrides,
}) async {
final now = clock.now();
final existing = id == null ? null : await templateRepository.findById(id);
if (id != null && existing == null) {
throw const DomainException('Workout template not found.');
}
final templateId = id ?? ids.newId();
final metadata =
existing?.metadata.touch(now) ??
EntityMetadata(
id: templateId,
createdAt: now,
updatedAt: now,
originDeviceId: originDeviceId,
);
final templatePrograms = <WorkoutTemplateProgram>[];
final programIdsByClientKey = <String, String>{};
for (var index = 0; index < programs.length; index++) {
final input = programs[index];
final programMetadata =
input.existingMetadata?.touch(now) ??
_newMetadata(ids, originDeviceId, now);
programIdsByClientKey[input.clientKey] = programMetadata.id;
templatePrograms.add(
WorkoutTemplateProgram(
metadata: programMetadata,
workoutTemplateId: templateId,
sourceProgramId: input.sourceProgramId,
position: index,
programNameSnapshot: input.programNameSnapshot,
defaultRestSecondsSnapshot: input.defaultRestSecondsSnapshot,
programSnapshotJson: input.programSnapshotJson,
),
);
}
final templateOverrides = <WorkoutTemplateExerciseOverride>[];
for (final input in overrides) {
final workoutTemplateProgramId =
programIdsByClientKey[input.workoutTemplateProgramClientKey];
if (workoutTemplateProgramId == null) {
throw const DomainException('Workout template program not found.');
}
templateOverrides.add(
WorkoutTemplateExerciseOverride(
metadata:
input.existingMetadata?.touch(now) ??
_newMetadata(ids, originDeviceId, now),
workoutTemplateProgramId: workoutTemplateProgramId,
snapshotProgramExerciseId: input.snapshotProgramExerciseId,
setsCountOverride: input.setsCountOverride,
targetTimeSecondsOverride: input.targetTimeSecondsOverride,
targetRepsOverride: input.targetRepsOverride,
targetScoreOverride: input.targetScoreOverride,
),
);
}
final template = WorkoutTemplate(
metadata: metadata,
name: name,
lastStartedAt: existing?.lastStartedAt,
programs: templatePrograms,
overrides: templateOverrides,
);
await templateRepository.replaceComposition(template, now);
return template;
}
Future<WorkoutTemplateProgram> addProgramSnapshot({
required String workoutTemplateId,
required String programId,
@ -457,6 +537,44 @@ final class WorkoutTemplateUseCases {
}
}
final class WorkoutTemplateProgramConfig {
const WorkoutTemplateProgramConfig({
required this.clientKey,
this.existingMetadata,
this.sourceProgramId,
required this.programNameSnapshot,
required this.defaultRestSecondsSnapshot,
required this.programSnapshotJson,
});
final String clientKey;
final EntityMetadata? existingMetadata;
final String? sourceProgramId;
final String programNameSnapshot;
final int defaultRestSecondsSnapshot;
final String programSnapshotJson;
}
final class WorkoutTemplateExerciseOverrideConfig {
const WorkoutTemplateExerciseOverrideConfig({
this.existingMetadata,
required this.workoutTemplateProgramClientKey,
required this.snapshotProgramExerciseId,
this.setsCountOverride,
this.targetTimeSecondsOverride,
this.targetRepsOverride,
this.targetScoreOverride,
});
final EntityMetadata? existingMetadata;
final String workoutTemplateProgramClientKey;
final String snapshotProgramExerciseId;
final int? setsCountOverride;
final int? targetTimeSecondsOverride;
final int? targetRepsOverride;
final double? targetScoreOverride;
}
final class ActiveWorkoutSessionUseCases {
const ActiveWorkoutSessionUseCases({
required this.sessionRepository,