feat(ui): écran Création/édition de programme

Ajoute l'écran de programme (presentation/program_screen.dart) et un
écran d'accueil (presentation/home_screen.dart) pour la navigation,
avec les ajustements application/domaine/repositories nécessaires.
flutter analyze propre, 11/11 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:51:49 +02:00
parent 00ae95f5c0
commit 1ae6248fb4
11 changed files with 1218 additions and 11 deletions

View File

@ -6,17 +6,20 @@ final class AppBootstrap {
required this.database,
required this.exerciseUseCases,
required this.mediaUseCases,
required this.programUseCases,
});
final AppDatabase database;
final ExerciseUseCases exerciseUseCases;
final MediaUseCases mediaUseCases;
final ProgramUseCases programUseCases;
static Future<AppBootstrap> create() async {
final database = AppDatabase.open();
await database.customSelect('SELECT 1').get();
final exerciseRepository = DriftExerciseRepository(database);
final mediaRepository = DriftMediaAssetRepository(database);
final programRepository = DriftProgramRepository(database);
final ids = LocalIdGenerator();
const clock = SystemClock();
const originDeviceId = 'local-device';
@ -37,6 +40,13 @@ final class AppBootstrap {
ids: ids,
originDeviceId: originDeviceId,
),
programUseCases: ProgramUseCases(
programRepository: programRepository,
exerciseRepository: exerciseRepository,
clock: clock,
ids: ids,
originDeviceId: originDeviceId,
),
);
}

View File

@ -59,8 +59,10 @@ abstract interface class MediaAssetRepository {
abstract interface class ProgramRepository {
Future<Program?> findById(String id);
Future<List<Program>> listActive();
Future<void> save(Program program);
Future<void> saveExercise(ProgramExercise exercise);
Future<void> replaceExercises(Program program, DateTime deletedAt);
}
abstract interface class WorkoutTemplateRepository {

View File

@ -227,6 +227,10 @@ final class ProgramUseCases {
final IdGenerator ids;
final String originDeviceId;
Future<Program?> findById(String id) => programRepository.findById(id);
Future<List<Program>> listActive() => programRepository.listActive();
Future<Program> create({
required String name,
required int defaultRestSeconds,
@ -241,6 +245,69 @@ final class ProgramUseCases {
return program;
}
Future<Program> saveConfigured({
String? id,
required String name,
required int defaultRestSeconds,
required List<ProgramExerciseConfig> exercises,
}) async {
final now = clock.now();
final existing = id == null ? null : await programRepository.findById(id);
if (id != null && existing == null) {
throw const DomainException('Program not found.');
}
final programId = id ?? ids.newId();
final metadata =
existing?.metadata.touch(now) ??
EntityMetadata(
id: programId,
createdAt: now,
updatedAt: now,
originDeviceId: originDeviceId,
);
final programExercises = <ProgramExercise>[];
for (var index = 0; index < exercises.length; index++) {
final input = exercises[index];
final exerciseMetadata =
input.existingMetadata?.touch(now) ??
_newMetadata(ids, originDeviceId, now);
programExercises.add(
ProgramExercise(
metadata: exerciseMetadata,
programId: programId,
sourceExerciseId: input.sourceExerciseId,
position: index,
exerciseNameSnapshot: input.exerciseNameSnapshot,
exerciseDescriptionSnapshot: input.exerciseDescriptionSnapshot,
exerciseImageMediaIdSnapshot: input.exerciseImageMediaIdSnapshot,
exerciseVideoMediaIdSnapshot: input.exerciseVideoMediaIdSnapshot,
exerciseArchivedSnapshot: input.exerciseArchivedSnapshot,
availableTimeSnapshot: input.availableTimeSnapshot,
availableRepsSnapshot: input.availableRepsSnapshot,
availableScoreSnapshot: input.availableScoreSnapshot,
scoreLabelSnapshot: input.scoreLabelSnapshot,
scoreUnitSnapshot: input.scoreUnitSnapshot,
setsCount: input.setsCount,
timeEnabled: input.enabledMeasures.contains(WorkoutMeasure.time),
repsEnabled: input.enabledMeasures.contains(WorkoutMeasure.reps),
scoreEnabled: input.enabledMeasures.contains(WorkoutMeasure.score),
targetTimeSeconds: input.targetTimeSeconds,
targetReps: input.targetReps,
targetScore: input.targetScore,
restSecondsOverride: input.restSecondsOverride,
),
);
}
final program = Program(
metadata: metadata,
name: name,
defaultRestSeconds: defaultRestSeconds,
exercises: programExercises,
);
await programRepository.replaceExercises(program, now);
return program;
}
Future<ProgramExercise> addExercise({
required String programId,
required String exerciseId,
@ -274,6 +341,48 @@ final class ProgramUseCases {
}
}
final class ProgramExerciseConfig {
const ProgramExerciseConfig({
this.existingMetadata,
this.sourceExerciseId,
required this.exerciseNameSnapshot,
this.exerciseDescriptionSnapshot,
this.exerciseImageMediaIdSnapshot,
this.exerciseVideoMediaIdSnapshot,
this.exerciseArchivedSnapshot = false,
required this.availableTimeSnapshot,
required this.availableRepsSnapshot,
required this.availableScoreSnapshot,
this.scoreLabelSnapshot,
this.scoreUnitSnapshot,
required this.setsCount,
required this.enabledMeasures,
this.targetTimeSeconds,
this.targetReps,
this.targetScore,
this.restSecondsOverride,
});
final EntityMetadata? existingMetadata;
final String? sourceExerciseId;
final String exerciseNameSnapshot;
final String? exerciseDescriptionSnapshot;
final String? exerciseImageMediaIdSnapshot;
final String? exerciseVideoMediaIdSnapshot;
final bool exerciseArchivedSnapshot;
final bool availableTimeSnapshot;
final bool availableRepsSnapshot;
final bool availableScoreSnapshot;
final String? scoreLabelSnapshot;
final String? scoreUnitSnapshot;
final int setsCount;
final Set<WorkoutMeasure> enabledMeasures;
final int? targetTimeSeconds;
final int? targetReps;
final double? targetScore;
final int? restSecondsOverride;
}
final class WorkoutTemplateUseCases {
const WorkoutTemplateUseCases({
required this.templateRepository,