feat(ui): integre affichage stats par scope et selection des types metier d'exercice cote telephone

UI telephone pour #179 (graphes/statistiques par etape, serie, exercice, seance) et #183 (choix des types metier d'exercice, hors pont Health Services).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 14:55:52 +02:00
parent fdaa518fa9
commit 36691643d7
8 changed files with 447 additions and 59 deletions

View File

@ -30,6 +30,7 @@ final class WorkoutExecutionScreen extends StatefulWidget {
this.videoMediaBuilder,
this.stepUseCases,
this.sensorUseCases,
this.telemetryUseCases,
this.stepAudioCuePlayer,
this.watchAlertPublisher,
super.key,
@ -46,6 +47,7 @@ final class WorkoutExecutionScreen extends StatefulWidget {
final Future<MediaAsset?> Function(String id)? mediaAssetLoader;
final VideoMediaBuilder? videoMediaBuilder;
final ActiveWorkoutSensorUseCases? sensorUseCases;
final WorkoutTelemetryUseCases? telemetryUseCases;
final ExerciseStepAudioCuePlayer? stepAudioCuePlayer;
final WatchAlertPublisher? watchAlertPublisher;
@ -274,11 +276,11 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
: null,
),
),
if (_hasLiveSensorMetrics(_sensorState)) ...[
if (_shouldShowLiveSensorBar) ...[
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
child: _LiveSensorBar(
sensorState: _sensorState!,
sensorState: _sensorState,
muted: _isSensorStateStale(_sensorState),
),
),
@ -405,10 +407,10 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text('Repos avant la prochaine série'),
if (_hasLiveSensorMetrics(_sensorState)) ...[
if (_shouldShowLiveSensorBar) ...[
const SizedBox(height: 12),
_LiveSensorBar(
sensorState: _sensorState!,
sensorState: _sensorState,
muted: _isSensorStateStale(_sensorState),
),
],
@ -485,10 +487,10 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('Pause', style: Theme.of(context).textTheme.headlineMedium),
if (_hasLiveSensorMetrics(_sensorState)) ...[
if (_shouldShowLiveSensorBar) ...[
const SizedBox(height: 12),
_LiveSensorBar(
sensorState: _sensorState!,
sensorState: _sensorState,
muted: _isSensorStateStale(_sensorState),
),
],
@ -638,6 +640,8 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
return hasGlobalTimer && !_hasSetExecutionStarted;
}
bool get _shouldShowLiveSensorBar => widget.sensorUseCases != null;
bool get _usesSetTimerForStopwatchScore {
return _exercise.timeEnabled && _exercise.stopwatchScoreEnabled;
}
@ -1837,6 +1841,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
workoutTemplateUseCases: widget.workoutTemplateUseCases,
activeUseCases: widget.activeUseCases,
stepUseCases: widget.stepUseCases,
telemetryUseCases: widget.telemetryUseCases,
closeUseCase: widget.closeUseCase,
),
),
@ -1894,6 +1899,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
initialSession: session,
activeUseCases: widget.activeUseCases,
stepUseCases: widget.stepUseCases,
telemetryUseCases: widget.telemetryUseCases,
closeUseCase: widget.closeUseCase,
historyUseCases: widget.historyUseCases,
workoutTemplateUseCases: widget.workoutTemplateUseCases,
@ -2306,7 +2312,7 @@ final class _ExecutionAppBarTitle extends StatelessWidget {
final class _LiveSensorBar extends StatelessWidget {
const _LiveSensorBar({required this.sensorState, required this.muted});
final ActiveWorkoutSensorState sensorState;
final ActiveWorkoutSensorState? sensorState;
final bool muted;
@override
@ -2318,24 +2324,21 @@ final class _LiveSensorBar extends StatelessWidget {
runSpacing: 4,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
if (sensorState.latestHeartRateBpm case final heartRate?)
_LiveSensorPill(
icon: Icons.favorite,
label: 'FC $heartRate bpm',
muted: false,
),
if (_distanceLabel(sensorState) case final distanceLabel?)
_LiveSensorPill(
icon: Icons.directions_run,
label: distanceLabel,
muted: false,
),
if (_caloriesLabel(sensorState) case final caloriesLabel?)
_LiveSensorPill(
icon: Icons.local_fire_department,
label: caloriesLabel,
muted: false,
),
_LiveSensorPill(
icon: Icons.favorite,
label: _heartRateMetricLabel(sensorState),
muted: sensorState?.latestHeartRateBpm == null,
),
_LiveSensorPill(
icon: Icons.directions_run,
label: _distanceMetricLabel(sensorState),
muted: _distanceLabel(sensorState) == null,
),
_LiveSensorPill(
icon: Icons.local_fire_department,
label: _caloriesMetricLabel(sensorState),
muted: _caloriesLabel(sensorState) == null,
),
],
),
);
@ -5320,6 +5323,29 @@ bool _isSensorStateStale(ActiveWorkoutSensorState? state) {
const Duration(seconds: 15);
}
String _heartRateMetricLabel(ActiveWorkoutSensorState? state) {
final heartRate = state?.latestHeartRateBpm;
if (heartRate != null) {
return 'FC $heartRate bpm';
}
return _sensorUnavailableLabel(state);
}
String _distanceMetricLabel(ActiveWorkoutSensorState? state) {
return _distanceLabel(state) ?? _sensorUnavailableLabel(state);
}
String _caloriesMetricLabel(ActiveWorkoutSensorState? state) {
return _caloriesLabel(state) ?? _sensorUnavailableLabel(state);
}
String _sensorUnavailableLabel(ActiveWorkoutSensorState? state) {
if (state == null) {
return 'Mesure en attente';
}
return 'Donnée indisponible';
}
String? _distanceLabel(ActiveWorkoutSensorState? state) {
final meters = state?.latestDistanceMeters;
if (meters == null || meters < 0) {
@ -5339,13 +5365,6 @@ String? _caloriesLabel(ActiveWorkoutSensorState? state) {
return null;
}
bool _hasLiveSensorMetrics(ActiveWorkoutSensorState? state) {
return state != null &&
(state.latestHeartRateBpm != null ||
state.latestDistanceMeters != null ||
_caloriesLabel(state) != null);
}
String _formatStepCountdown(Duration duration) {
final totalSeconds = (duration.inMilliseconds / 1000)
.ceil()