feat(exercice): exécution de séance avec module séquence et bips (ticket #60)

Ajoute le lecteur de bips d'étapes (presentation/exercise_step_audio.dart,
port ExerciseStepAudioCuePlayer injectable en test, jamais le vrai
audioplayers en test) et les fichiers audio associés
(assets/audio/step_countdown_short.wav, step_completion_long.wav).
Câble ActiveExerciseStepUseCases dans app_bootstrap.dart et l'expose à
WorkoutExecutionScreen. Les appels audio réels sont protégés par un
try/catch qui log sans jamais bloquer la progression d'étape. flutter
pub get OK (ajout audioplayers), analyze propre (mêmes infos
préexistantes + 2 nouvelles use_build_context_synchronously, non
bloquantes), 100/100 tests verts, build APK debug validé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 20:07:56 +02:00
parent 83c87d9a42
commit 6947cee30c
13 changed files with 1459 additions and 0 deletions

View File

@ -7,6 +7,7 @@ abstract interface class AppDependencies {
ProgramUseCases get programUseCases;
WorkoutTemplateUseCases get workoutTemplateUseCases;
ActiveWorkoutSessionUseCases get activeWorkoutSessionUseCases;
ActiveExerciseStepUseCases get activeExerciseStepUseCases;
CloseWorkoutSessionUseCase get closeWorkoutSessionUseCase;
WorkoutHistoryUseCases get workoutHistoryUseCases;
}
@ -19,6 +20,7 @@ final class AppBootstrap implements AppDependencies {
required this.programUseCases,
required this.workoutTemplateUseCases,
required this.activeWorkoutSessionUseCases,
required this.activeExerciseStepUseCases,
required this.closeWorkoutSessionUseCase,
required this.workoutHistoryUseCases,
required this.syncGateway,
@ -36,6 +38,8 @@ final class AppBootstrap implements AppDependencies {
@override
final ActiveWorkoutSessionUseCases activeWorkoutSessionUseCases;
@override
final ActiveExerciseStepUseCases activeExerciseStepUseCases;
@override
final CloseWorkoutSessionUseCase closeWorkoutSessionUseCase;
@override
final WorkoutHistoryUseCases workoutHistoryUseCases;
@ -93,6 +97,12 @@ final class AppBootstrap implements AppDependencies {
ids: ids,
originDeviceId: originDeviceId,
),
activeExerciseStepUseCases: ActiveExerciseStepUseCases(
sessionRepository: activeSessionRepository,
clock: clock,
ids: ids,
originDeviceId: originDeviceId,
),
closeWorkoutSessionUseCase: CloseWorkoutSessionUseCase(
sessionRepository: activeSessionRepository,
historyRepository: historyRepository,

View File

@ -0,0 +1,49 @@
import 'package:audioplayers/audioplayers.dart';
abstract interface class ExerciseStepAudioCuePlayer {
Future<void> playShortCountdownBeep();
Future<void> playLongCompletionBeep();
Future<void> dispose();
}
final class AudioplayersExerciseStepAudioCuePlayer
implements ExerciseStepAudioCuePlayer {
AudioplayersExerciseStepAudioCuePlayer();
final AudioPlayer _player = AudioPlayer();
@override
Future<void> playShortCountdownBeep() {
return _play('audio/step_countdown_short.wav');
}
@override
Future<void> playLongCompletionBeep() {
return _play('audio/step_completion_long.wav');
}
Future<void> _play(String assetPath) async {
await _player.stop();
await _player.play(AssetSource(assetPath));
}
@override
Future<void> dispose() {
return _player.dispose();
}
}
final class NoOpExerciseStepAudioCuePlayer implements ExerciseStepAudioCuePlayer {
const NoOpExerciseStepAudioCuePlayer();
@override
Future<void> playShortCountdownBeep() async {}
@override
Future<void> playLongCompletionBeep() async {}
@override
Future<void> dispose() async {}
}

View File

@ -14,12 +14,14 @@ final class HistoryListScreen extends StatefulWidget {
required this.activeUseCases,
required this.closeUseCase,
this.mediaUseCases,
this.stepUseCases,
super.key,
});
final WorkoutHistoryUseCases historyUseCases;
final WorkoutTemplateUseCases workoutTemplateUseCases;
final ActiveWorkoutSessionUseCases activeUseCases;
final ActiveExerciseStepUseCases? stepUseCases;
final CloseWorkoutSessionUseCase closeUseCase;
final MediaUseCases? mediaUseCases;
@ -94,6 +96,7 @@ final class _HistoryListScreenState extends State<HistoryListScreen> {
historyUseCases: widget.historyUseCases,
workoutTemplateUseCases: widget.workoutTemplateUseCases,
activeUseCases: widget.activeUseCases,
stepUseCases: widget.stepUseCases,
closeUseCase: widget.closeUseCase,
mediaUseCases: widget.mediaUseCases,
),
@ -115,6 +118,7 @@ final class HistoryDetailScreen extends StatelessWidget {
required this.activeUseCases,
required this.closeUseCase,
this.mediaUseCases,
this.stepUseCases,
super.key,
});
@ -122,6 +126,7 @@ final class HistoryDetailScreen extends StatelessWidget {
final WorkoutHistoryUseCases historyUseCases;
final WorkoutTemplateUseCases workoutTemplateUseCases;
final ActiveWorkoutSessionUseCases activeUseCases;
final ActiveExerciseStepUseCases? stepUseCases;
final CloseWorkoutSessionUseCase closeUseCase;
final MediaUseCases? mediaUseCases;
@ -193,6 +198,7 @@ final class HistoryDetailScreen extends StatelessWidget {
builder: (context) => WorkoutExecutionScreen(
initialSession: session,
activeUseCases: activeUseCases,
stepUseCases: stepUseCases,
closeUseCase: closeUseCase,
historyUseCases: historyUseCases,
workoutTemplateUseCases: workoutTemplateUseCases,

View File

@ -152,6 +152,7 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
widget.bootstrap.workoutTemplateUseCases,
programUseCases: widget.bootstrap.programUseCases,
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
stepUseCases: widget.bootstrap.activeExerciseStepUseCases,
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
mediaUseCases: widget.bootstrap.mediaUseCases,
@ -170,6 +171,7 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
workoutTemplateUseCases:
widget.bootstrap.workoutTemplateUseCases,
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
stepUseCases: widget.bootstrap.activeExerciseStepUseCases,
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
mediaUseCases: widget.bootstrap.mediaUseCases,
),
@ -187,6 +189,7 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
builder: (context) => WorkoutExecutionScreen(
initialSession: session,
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
stepUseCases: widget.bootstrap.activeExerciseStepUseCases,
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
workoutTemplateUseCases: widget.bootstrap.workoutTemplateUseCases,

View File

@ -1,4 +1,5 @@
export 'game_time_app.dart';
export 'exercise_step_audio.dart';
export 'exercise_library_screen.dart';
export 'history_screen.dart';
export 'home_screen.dart';

File diff suppressed because it is too large Load Diff

View File

@ -14,12 +14,14 @@ final class WorkoutTemplateListScreen extends StatefulWidget {
required this.closeUseCase,
required this.historyUseCases,
this.mediaUseCases,
this.stepUseCases,
super.key,
});
final WorkoutTemplateUseCases workoutTemplateUseCases;
final ProgramUseCases programUseCases;
final ActiveWorkoutSessionUseCases activeUseCases;
final ActiveExerciseStepUseCases? stepUseCases;
final CloseWorkoutSessionUseCase closeUseCase;
final WorkoutHistoryUseCases historyUseCases;
final MediaUseCases? mediaUseCases;
@ -140,6 +142,7 @@ final class _WorkoutTemplateListScreenState
builder: (context) => WorkoutExecutionScreen(
initialSession: session,
activeUseCases: widget.activeUseCases,
stepUseCases: widget.stepUseCases,
closeUseCase: widget.closeUseCase,
historyUseCases: widget.historyUseCases,
workoutTemplateUseCases: widget.workoutTemplateUseCases,