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

@ -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});