chore(wip): lot #165-169 validé QA + rework foreground #163 (KO - preuve device/toolchain insuffisante)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 17:59:43 +02:00
parent 917777e18b
commit bc533d6c45
32 changed files with 898 additions and 123 deletions

View File

@ -3670,6 +3670,9 @@ final class WatchCompanionCommandHandler implements WatchCommandIngress {
projection.primaryAction == WatchPrimaryAction.resumeSession,
WatchCommandType.startPreparedTimedStep =>
projection.primaryAction == WatchPrimaryAction.startPreparedTimedStep,
WatchCommandType.completeCurrentStep =>
projection.stepType == WatchStepType.reps &&
projection.stepTargetValue != null,
WatchCommandType.skipCurrentStep => projection.secondaryActions.contains(
WatchSecondaryAction.skipCurrentStep,
),
@ -3712,6 +3715,7 @@ final class WatchCompanionCommandHandler implements WatchCommandIngress {
WatchCommandType.startPreparedTimedStep => _startPreparedTimedStep(
session,
),
WatchCommandType.completeCurrentStep => _completeCurrentStep(session),
WatchCommandType.skipCurrentStep => _skipCurrentStep(session),
WatchCommandType.skipCurrentPassage => _skipCurrentPassage(session),
WatchCommandType.finishCurrentSet => _finishCurrentSet(
@ -3776,6 +3780,18 @@ final class WatchCompanionCommandHandler implements WatchCommandIngress {
return WatchCommandAck.accepted;
}
Future<WatchCommandAck> _completeCurrentStep(
ActiveWorkoutSession session,
) async {
await _stepUseCases.completeCurrentStep(
sessionId: session.metadata.id,
programIndex: session.currentProgramIndex,
exerciseIndex: session.currentExerciseIndex,
setIndex: session.currentSetIndex,
);
return WatchCommandAck.accepted;
}
Future<WatchCommandAck> _skipCurrentPassage(
ActiveWorkoutSession session,
) async {
@ -4151,6 +4167,12 @@ final class WatchSessionProjectionProjector {
: (stepState?.currentStepIndex ?? 0) + 1,
stepTotal: snapshot.steps.isEmpty ? null : snapshot.steps.length,
stepName: currentStep?.name,
stepType: switch (currentStep?.type) {
ExerciseStepType.time => WatchStepType.time,
ExerciseStepType.reps => WatchStepType.reps,
null => null,
},
stepTargetValue: currentStep?.defaultTargetValue,
dominantTimer: dominantTimer,
secondaryTimers: secondaryTimers,
primaryAction: _primaryAction(phase),
@ -4389,7 +4411,7 @@ List<WatchSecondaryAction> _secondaryActions({
];
}
String _statusLabel(
String? _statusLabel(
WatchSessionPhase phase,
WatchTimerProjection? dominantTimer,
) {
@ -4401,7 +4423,7 @@ String _statusLabel(
WatchSessionPhase.nextTimerReady => 'Chrono suivant prêt',
WatchSessionPhase.restRunning => 'Repos en cours',
WatchSessionPhase.restPaused => 'Repos en pause',
WatchSessionPhase.betweenSetsReady => 'Prêt pour la série suivante',
WatchSessionPhase.betweenSetsReady => null,
};
}
@ -4502,8 +4524,7 @@ bool _isNextTimerReady(
if (state == null ||
currentStep == null ||
state.status != ActiveExerciseStepProgressStatus.stoppedTimer ||
currentStep.type != ExerciseStepType.time ||
snapshot.autoStartNextTimedStepEffective) {
currentStep.type != ExerciseStepType.time) {
return false;
}
final previous = _previousStep(snapshot, state);

View File

@ -162,13 +162,9 @@ final class HistoryDetailScreen extends StatelessWidget {
Text(_formatDateTime(history.startedAt)),
const SizedBox(height: 4),
Text('Durée : ${_formatDurationMs(history.totalActiveMs)}'),
if (history.averageHeartRateBpm != null &&
history.maxHeartRateBpm != null) ...[
if (_hasWatchStats(history)) ...[
const SizedBox(height: 16),
_HistoryHeartRateSummary(
averageHeartRateBpm: history.averageHeartRateBpm!,
maxHeartRateBpm: history.maxHeartRateBpm!,
),
_HistoryWatchStatsSummary(history: history),
],
const SizedBox(height: 16),
for (final program in detail.programs) ...[
@ -315,52 +311,163 @@ final class HistoryDetailScreen extends StatelessWidget {
}
}
final class _HistoryHeartRateSummary extends StatelessWidget {
const _HistoryHeartRateSummary({
required this.averageHeartRateBpm,
required this.maxHeartRateBpm,
});
final class _HistoryWatchStatsSummary extends StatelessWidget {
const _HistoryWatchStatsSummary({required this.history});
final double averageHeartRateBpm;
final int maxHeartRateBpm;
final WorkoutHistory history;
@override
Widget build(BuildContext context) {
final minHeartRate = history.minHeartRateBpm;
final averageHeartRate = history.averageHeartRateBpm;
final maxHeartRate = history.maxHeartRateBpm;
final distance = history.totalDistanceMeters;
final calories = history.totalCaloriesKcal;
return CourtBlazerAccentPanel(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Fréquence cardiaque',
style: Theme.of(context).textTheme.titleSmall,
),
Text('Stats montre', style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _HistoryHeartRateMetric(
label: 'Moyenne',
value: '${averageHeartRateBpm.round()}',
),
),
const SizedBox(width: 12),
Expanded(
child: _HistoryHeartRateMetric(
label: 'Max',
value: '$maxHeartRateBpm',
),
if (minHeartRate != null ||
averageHeartRate != null ||
maxHeartRate != null) ...[
Text('Fréquence cardiaque'),
const SizedBox(height: 8),
Row(
children: [
if (minHeartRate != null)
Expanded(
child: _HistoryWatchMetric(
label: 'Min',
value: '$minHeartRate bpm',
),
),
if (minHeartRate != null && averageHeartRate != null)
const SizedBox(width: 8),
if (averageHeartRate != null)
Expanded(
child: _HistoryWatchMetric(
label: 'Moyenne',
value: '${averageHeartRate.round()} bpm',
),
),
if ((minHeartRate != null || averageHeartRate != null) &&
maxHeartRate != null)
const SizedBox(width: 8),
if (maxHeartRate != null)
Expanded(
child: _HistoryWatchMetric(
label: 'Max',
value: '$maxHeartRate bpm',
),
),
],
),
if (averageHeartRate != null && maxHeartRate != null) ...[
const SizedBox(height: 10),
_HistoryWatchStatsBars(
minHeartRateBpm: minHeartRate,
averageHeartRateBpm: averageHeartRate,
maxHeartRateBpm: maxHeartRate,
),
],
),
],
if (distance != null || calories != null) ...[
const SizedBox(height: 12),
Row(
children: [
if (distance != null)
Expanded(
child: _HistoryWatchMetric(
label: 'Distance',
value: _formatHistoryDistance(distance),
),
),
if (distance != null && calories != null)
const SizedBox(width: 8),
if (calories != null)
Expanded(
child: _HistoryWatchMetric(
label: 'Calories',
value: '${calories.round()} kcal',
),
),
],
),
],
],
),
);
}
}
final class _HistoryHeartRateMetric extends StatelessWidget {
const _HistoryHeartRateMetric({required this.label, required this.value});
final class _HistoryWatchStatsBars extends StatelessWidget {
const _HistoryWatchStatsBars({
required this.minHeartRateBpm,
required this.averageHeartRateBpm,
required this.maxHeartRateBpm,
});
final int? minHeartRateBpm;
final double averageHeartRateBpm;
final int maxHeartRateBpm;
@override
Widget build(BuildContext context) {
final max = maxHeartRateBpm.toDouble().clamp(1, double.infinity);
final values = [
if (minHeartRateBpm != null) ('Min', minHeartRateBpm!.toDouble()),
('Moy', averageHeartRateBpm),
('Max', maxHeartRateBpm.toDouble()),
];
return Semantics(
label: 'Graphique stats montre',
key: const ValueKey('history-watch-stats-graph'),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
for (final item in values) ...[
Expanded(
child: Column(
children: [
SizedBox(
height: 52,
child: Align(
alignment: Alignment.bottomCenter,
child: FractionallySizedBox(
heightFactor: (item.$2 / max).clamp(0.08, 1),
widthFactor: 0.62,
child: DecoratedBox(
decoration: BoxDecoration(
color: const Color(0xFFC9A24A),
borderRadius: BorderRadius.circular(4),
),
),
),
),
),
const SizedBox(height: 4),
Text(
item.$1,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.labelSmall,
),
],
),
),
if (item != values.last) const SizedBox(width: 8),
],
],
),
);
}
}
final class _HistoryWatchMetric extends StatelessWidget {
const _HistoryWatchMetric({required this.label, required this.value});
final String label;
final String value;
@ -373,14 +480,8 @@ final class _HistoryHeartRateMetric extends StatelessWidget {
Text(label, style: Theme.of(context).textTheme.labelMedium),
RichText(
text: TextSpan(
style: AppTextStyles.scoreNumber(context),
children: [
TextSpan(text: value),
TextSpan(
text: ' bpm',
style: Theme.of(context).textTheme.bodySmall,
),
],
style: AppTextStyles.scoreNumber(context).copyWith(fontSize: 24),
children: [TextSpan(text: value)],
),
),
],
@ -388,6 +489,21 @@ final class _HistoryHeartRateMetric extends StatelessWidget {
}
}
bool _hasWatchStats(WorkoutHistory history) {
return history.minHeartRateBpm != null ||
history.averageHeartRateBpm != null ||
history.maxHeartRateBpm != null ||
history.totalDistanceMeters != null ||
history.totalCaloriesKcal != null;
}
String _formatHistoryDistance(double meters) {
if (meters >= 1000) {
return '${(meters / 1000).toStringAsFixed(2)} km';
}
return '${meters.round()} m';
}
final class HistoryDetailData {
const HistoryDetailData({required this.programs});