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:
@ -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);
|
||||
|
||||
@ -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});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user