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:
@ -361,7 +361,7 @@ final class ExerciseListTile extends StatelessWidget {
|
|||||||
spacing: 6,
|
spacing: 6,
|
||||||
runSpacing: 6,
|
runSpacing: 6,
|
||||||
children: [
|
children: [
|
||||||
_CategoryBadge(category: exercise.category),
|
_BusinessTypeBadge(types: exercise.effectiveBusinessTypes),
|
||||||
...exercise.availableMeasures.map(
|
...exercise.availableMeasures.map(
|
||||||
(measure) => MeasureBadge(
|
(measure) => MeasureBadge(
|
||||||
measure: measure,
|
measure: measure,
|
||||||
@ -398,17 +398,19 @@ final class ExerciseListTile extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class _CategoryBadge extends StatelessWidget {
|
final class _BusinessTypeBadge extends StatelessWidget {
|
||||||
const _CategoryBadge({required this.category});
|
const _BusinessTypeBadge({required this.types});
|
||||||
|
|
||||||
final ExerciseCategory category;
|
final List<BusinessExerciseType> types;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Chip(
|
final label = types.isEmpty
|
||||||
visualDensity: VisualDensity.compact,
|
? 'Libre / autre'
|
||||||
label: Text(category.label),
|
: types.length == 1
|
||||||
);
|
? types.single.label
|
||||||
|
: '${types.first.label} +${types.length - 1}';
|
||||||
|
return Chip(visualDensity: VisualDensity.compact, label: Text(label));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -487,6 +489,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
var _hasReps = false;
|
var _hasReps = false;
|
||||||
var _hasScore = false;
|
var _hasScore = false;
|
||||||
var _scoreInputMode = ScoreInputMode.manual;
|
var _scoreInputMode = ScoreInputMode.manual;
|
||||||
|
var _businessTypes = <BusinessExerciseType>[];
|
||||||
var _saving = false;
|
var _saving = false;
|
||||||
var _importingImage = false;
|
var _importingImage = false;
|
||||||
var _importingVideo = false;
|
var _importingVideo = false;
|
||||||
@ -529,6 +532,9 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
_hasReps = exercise?.hasRepsMeasure ?? false;
|
_hasReps = exercise?.hasRepsMeasure ?? false;
|
||||||
_hasScore = exercise?.hasScoreMeasure ?? false;
|
_hasScore = exercise?.hasScoreMeasure ?? false;
|
||||||
_scoreInputMode = exercise?.scoreInputMode ?? ScoreInputMode.manual;
|
_scoreInputMode = exercise?.scoreInputMode ?? ScoreInputMode.manual;
|
||||||
|
_businessTypes = List<BusinessExerciseType>.of(
|
||||||
|
exercise?.effectiveBusinessTypes ?? const [],
|
||||||
|
);
|
||||||
_stepsEnabled = exercise?.steps.isNotEmpty ?? false;
|
_stepsEnabled = exercise?.steps.isNotEmpty ?? false;
|
||||||
_autoStartNextTimedStep = exercise?.autoStartNextTimedStep ?? true;
|
_autoStartNextTimedStep = exercise?.autoStartNextTimedStep ?? true;
|
||||||
_tags = List<String>.of(exercise?.tags ?? const []);
|
_tags = List<String>.of(exercise?.tags ?? const []);
|
||||||
@ -781,6 +787,8 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
],
|
],
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
_buildExerciseStepsSection(context),
|
_buildExerciseStepsSection(context),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildBusinessTypesSection(context),
|
||||||
if (_warning != null) ...[
|
if (_warning != null) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
MaterialBanner(
|
MaterialBanner(
|
||||||
@ -811,6 +819,51 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildBusinessTypesSection(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Type d’exercice', style: Theme.of(context).textTheme.titleSmall),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
FilterChip(
|
||||||
|
label: const Text('Libre / autre'),
|
||||||
|
selected: _businessTypes.isEmpty,
|
||||||
|
onSelected: (_) => setState(() => _businessTypes = []),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
for (final type in BusinessExerciseType.values) ...[
|
||||||
|
FilterChip(
|
||||||
|
label: Text(type.label),
|
||||||
|
selected: _businessTypes.contains(type),
|
||||||
|
onSelected: (selected) {
|
||||||
|
setState(() {
|
||||||
|
final next = List<BusinessExerciseType>.of(
|
||||||
|
_businessTypes,
|
||||||
|
);
|
||||||
|
if (selected) {
|
||||||
|
if (!next.contains(type)) {
|
||||||
|
next.add(type);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
next.remove(type);
|
||||||
|
}
|
||||||
|
_businessTypes = next;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildExerciseStepsSection(BuildContext context) {
|
Widget _buildExerciseStepsSection(BuildContext context) {
|
||||||
final reachedLimit = _stepDrafts.length >= 8;
|
final reachedLimit = _stepDrafts.length >= 8;
|
||||||
return Column(
|
return Column(
|
||||||
@ -1467,6 +1520,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
defaultTargetScore: defaultTargetScore,
|
defaultTargetScore: defaultTargetScore,
|
||||||
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
|
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
|
||||||
autoStartNextTimedStep: _autoStartNextTimedStep,
|
autoStartNextTimedStep: _autoStartNextTimedStep,
|
||||||
|
businessTypes: _businessTypes,
|
||||||
steps: steps,
|
steps: steps,
|
||||||
tags: _tags,
|
tags: _tags,
|
||||||
);
|
);
|
||||||
@ -1489,6 +1543,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
defaultTargetScore: defaultTargetScore,
|
defaultTargetScore: defaultTargetScore,
|
||||||
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
|
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
|
||||||
autoStartNextTimedStep: _autoStartNextTimedStep,
|
autoStartNextTimedStep: _autoStartNextTimedStep,
|
||||||
|
businessTypes: _businessTypes,
|
||||||
steps: steps,
|
steps: steps,
|
||||||
tags: _tags,
|
tags: _tags,
|
||||||
);
|
);
|
||||||
@ -2022,17 +2077,18 @@ extension on WorkoutMeasure {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on ExerciseCategory {
|
extension on BusinessExerciseType {
|
||||||
String get label {
|
String get label {
|
||||||
return switch (this) {
|
return switch (this) {
|
||||||
ExerciseCategory.shoot => 'Tir',
|
BusinessExerciseType.shoot => 'Tir',
|
||||||
ExerciseCategory.freeThrows => 'Lancers francs',
|
BusinessExerciseType.freeThrows => 'Lancers francs',
|
||||||
ExerciseCategory.dribble => 'Dribble',
|
BusinessExerciseType.dribble => 'Dribble',
|
||||||
ExerciseCategory.finishing => 'Finition',
|
BusinessExerciseType.finishing => 'Finition',
|
||||||
ExerciseCategory.conditioning => 'Condition physique',
|
BusinessExerciseType.conditioning => 'Condition physique',
|
||||||
ExerciseCategory.defense => 'Défense',
|
BusinessExerciseType.defense => 'Défense',
|
||||||
ExerciseCategory.mobility => 'Mobilité',
|
BusinessExerciseType.mobility => 'Mobilité',
|
||||||
ExerciseCategory.uncategorized => 'Non catégorisé',
|
BusinessExerciseType.highIntensity => 'Haute intensité',
|
||||||
|
BusinessExerciseType.recovery => 'Récupération',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
@ -16,6 +17,7 @@ final class HistoryListScreen extends StatefulWidget {
|
|||||||
this.mediaUseCases,
|
this.mediaUseCases,
|
||||||
this.stepUseCases,
|
this.stepUseCases,
|
||||||
this.sensorUseCases,
|
this.sensorUseCases,
|
||||||
|
this.telemetryUseCases,
|
||||||
this.performanceReferenceUseCase,
|
this.performanceReferenceUseCase,
|
||||||
this.onOpenProgression,
|
this.onOpenProgression,
|
||||||
this.watchAlertPublisher,
|
this.watchAlertPublisher,
|
||||||
@ -27,6 +29,7 @@ final class HistoryListScreen extends StatefulWidget {
|
|||||||
final ActiveWorkoutSessionUseCases activeUseCases;
|
final ActiveWorkoutSessionUseCases activeUseCases;
|
||||||
final ActiveExerciseStepUseCases? stepUseCases;
|
final ActiveExerciseStepUseCases? stepUseCases;
|
||||||
final ActiveWorkoutSensorUseCases? sensorUseCases;
|
final ActiveWorkoutSensorUseCases? sensorUseCases;
|
||||||
|
final WorkoutTelemetryUseCases? telemetryUseCases;
|
||||||
final ExercisePerformanceReferenceUseCase? performanceReferenceUseCase;
|
final ExercisePerformanceReferenceUseCase? performanceReferenceUseCase;
|
||||||
final CloseWorkoutSessionUseCase closeUseCase;
|
final CloseWorkoutSessionUseCase closeUseCase;
|
||||||
final MediaUseCases? mediaUseCases;
|
final MediaUseCases? mediaUseCases;
|
||||||
@ -115,6 +118,7 @@ final class _HistoryListScreenState extends State<HistoryListScreen> {
|
|||||||
activeUseCases: widget.activeUseCases,
|
activeUseCases: widget.activeUseCases,
|
||||||
stepUseCases: widget.stepUseCases,
|
stepUseCases: widget.stepUseCases,
|
||||||
sensorUseCases: widget.sensorUseCases,
|
sensorUseCases: widget.sensorUseCases,
|
||||||
|
telemetryUseCases: widget.telemetryUseCases,
|
||||||
performanceReferenceUseCase: widget.performanceReferenceUseCase,
|
performanceReferenceUseCase: widget.performanceReferenceUseCase,
|
||||||
closeUseCase: widget.closeUseCase,
|
closeUseCase: widget.closeUseCase,
|
||||||
mediaUseCases: widget.mediaUseCases,
|
mediaUseCases: widget.mediaUseCases,
|
||||||
@ -140,6 +144,7 @@ final class HistoryDetailScreen extends StatelessWidget {
|
|||||||
this.mediaUseCases,
|
this.mediaUseCases,
|
||||||
this.stepUseCases,
|
this.stepUseCases,
|
||||||
this.sensorUseCases,
|
this.sensorUseCases,
|
||||||
|
this.telemetryUseCases,
|
||||||
this.performanceReferenceUseCase,
|
this.performanceReferenceUseCase,
|
||||||
this.watchAlertPublisher,
|
this.watchAlertPublisher,
|
||||||
super.key,
|
super.key,
|
||||||
@ -151,6 +156,7 @@ final class HistoryDetailScreen extends StatelessWidget {
|
|||||||
final ActiveWorkoutSessionUseCases activeUseCases;
|
final ActiveWorkoutSessionUseCases activeUseCases;
|
||||||
final ActiveExerciseStepUseCases? stepUseCases;
|
final ActiveExerciseStepUseCases? stepUseCases;
|
||||||
final ActiveWorkoutSensorUseCases? sensorUseCases;
|
final ActiveWorkoutSensorUseCases? sensorUseCases;
|
||||||
|
final WorkoutTelemetryUseCases? telemetryUseCases;
|
||||||
final ExercisePerformanceReferenceUseCase? performanceReferenceUseCase;
|
final ExercisePerformanceReferenceUseCase? performanceReferenceUseCase;
|
||||||
final CloseWorkoutSessionUseCase closeUseCase;
|
final CloseWorkoutSessionUseCase closeUseCase;
|
||||||
final MediaUseCases? mediaUseCases;
|
final MediaUseCases? mediaUseCases;
|
||||||
@ -169,7 +175,10 @@ final class HistoryDetailScreen extends StatelessWidget {
|
|||||||
Text('Durée : ${_formatDurationMs(history.totalActiveMs)}'),
|
Text('Durée : ${_formatDurationMs(history.totalActiveMs)}'),
|
||||||
if (_hasWatchStats(history)) ...[
|
if (_hasWatchStats(history)) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_HistoryWatchStatsSummary(history: history),
|
_HistoryWatchStatsSummary(
|
||||||
|
history: history,
|
||||||
|
telemetryUseCases: telemetryUseCases,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
for (final program in detail.programs) ...[
|
for (final program in detail.programs) ...[
|
||||||
@ -318,9 +327,13 @@ final class HistoryDetailScreen extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class _HistoryWatchStatsSummary extends StatelessWidget {
|
final class _HistoryWatchStatsSummary extends StatelessWidget {
|
||||||
const _HistoryWatchStatsSummary({required this.history});
|
const _HistoryWatchStatsSummary({
|
||||||
|
required this.history,
|
||||||
|
required this.telemetryUseCases,
|
||||||
|
});
|
||||||
|
|
||||||
final WorkoutHistory history;
|
final WorkoutHistory history;
|
||||||
|
final WorkoutTelemetryUseCases? telemetryUseCases;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -335,6 +348,13 @@ final class _HistoryWatchStatsSummary extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text('Stats montre', style: Theme.of(context).textTheme.titleSmall),
|
Text('Stats montre', style: Theme.of(context).textTheme.titleSmall),
|
||||||
|
if (telemetryUseCases != null) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_HistoryWatchTelemetryGraph(
|
||||||
|
history: history,
|
||||||
|
telemetryUseCases: telemetryUseCases!,
|
||||||
|
),
|
||||||
|
],
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
if (minHeartRate != null ||
|
if (minHeartRate != null ||
|
||||||
averageHeartRate != null ||
|
averageHeartRate != null ||
|
||||||
@ -409,6 +429,231 @@ final class _HistoryWatchStatsSummary extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum _TelemetryScope { session, exercise, set, step }
|
||||||
|
|
||||||
|
final class _HistoryWatchTelemetryGraph extends StatefulWidget {
|
||||||
|
const _HistoryWatchTelemetryGraph({
|
||||||
|
required this.history,
|
||||||
|
required this.telemetryUseCases,
|
||||||
|
});
|
||||||
|
|
||||||
|
final WorkoutHistory history;
|
||||||
|
final WorkoutTelemetryUseCases telemetryUseCases;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_HistoryWatchTelemetryGraph> createState() =>
|
||||||
|
_HistoryWatchTelemetryGraphState();
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _HistoryWatchTelemetryGraphState
|
||||||
|
extends State<_HistoryWatchTelemetryGraph> {
|
||||||
|
_TelemetryScope _scope = _TelemetryScope.session;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder<List<WorkoutTelemetrySample>>(
|
||||||
|
future: widget.telemetryUseCases.listSamplesForHistory(widget.history),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState != ConnectionState.done) {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 120,
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final samples = snapshot.data ?? const <WorkoutTelemetrySample>[];
|
||||||
|
if (samples.isEmpty) {
|
||||||
|
return const _HistoryWatchMetric(
|
||||||
|
label: 'Courbe FC',
|
||||||
|
value: 'Donnée indisponible',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final scopedSamples = _samplesForTelemetryScope(samples, _scope);
|
||||||
|
final heartRatePoints = scopedSamples
|
||||||
|
.where((sample) => sample.heartRateBpm != null)
|
||||||
|
.toList();
|
||||||
|
final distance = _scopeDelta(
|
||||||
|
scopedSamples
|
||||||
|
.map((sample) => sample.distanceMeters)
|
||||||
|
.whereType<double>(),
|
||||||
|
);
|
||||||
|
final calories = _scopeDelta(
|
||||||
|
scopedSamples
|
||||||
|
.map((sample) => sample.caloriesKcal)
|
||||||
|
.whereType<double>(),
|
||||||
|
);
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
SegmentedButton<_TelemetryScope>(
|
||||||
|
showSelectedIcon: false,
|
||||||
|
segments: const [
|
||||||
|
ButtonSegment(
|
||||||
|
value: _TelemetryScope.session,
|
||||||
|
label: Text('Séance'),
|
||||||
|
),
|
||||||
|
ButtonSegment(
|
||||||
|
value: _TelemetryScope.exercise,
|
||||||
|
label: Text('Exercice'),
|
||||||
|
),
|
||||||
|
ButtonSegment(value: _TelemetryScope.set, label: Text('Série')),
|
||||||
|
ButtonSegment(
|
||||||
|
value: _TelemetryScope.step,
|
||||||
|
label: Text('Étape'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
selected: {_scope},
|
||||||
|
onSelectionChanged: (selection) =>
|
||||||
|
setState(() => _scope = selection.single),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
SizedBox(
|
||||||
|
height: 160,
|
||||||
|
child: heartRatePoints.length < 2
|
||||||
|
? const Center(child: Text('Mesure en attente'))
|
||||||
|
: CustomPaint(
|
||||||
|
key: const ValueKey('history-watch-stats-graph'),
|
||||||
|
painter: _HeartRateTelemetryPainter(
|
||||||
|
samples: heartRatePoints,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _HistoryWatchMetric(
|
||||||
|
label: 'Distance',
|
||||||
|
value: distance == null
|
||||||
|
? 'Donnée indisponible'
|
||||||
|
: _formatHistoryDistance(distance),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: _HistoryWatchMetric(
|
||||||
|
label: 'Calories',
|
||||||
|
value: calories == null
|
||||||
|
? 'Donnée indisponible'
|
||||||
|
: '${calories.round()} kcal',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<WorkoutTelemetrySample> _samplesForTelemetryScope(
|
||||||
|
List<WorkoutTelemetrySample> samples,
|
||||||
|
_TelemetryScope scope,
|
||||||
|
) {
|
||||||
|
int? firstNonNull(int? Function(WorkoutTelemetrySample sample) read) {
|
||||||
|
for (final sample in samples) {
|
||||||
|
final value = read(sample);
|
||||||
|
if (value != null) return value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final exercise = firstNonNull((sample) => sample.exerciseIndex);
|
||||||
|
final set = firstNonNull((sample) => sample.setIndex);
|
||||||
|
final step = firstNonNull((sample) => sample.stepIndex);
|
||||||
|
return samples.where((sample) {
|
||||||
|
return switch (scope) {
|
||||||
|
_TelemetryScope.session => true,
|
||||||
|
_TelemetryScope.exercise => sample.exerciseIndex == exercise,
|
||||||
|
_TelemetryScope.set =>
|
||||||
|
sample.exerciseIndex == exercise && sample.setIndex == set,
|
||||||
|
_TelemetryScope.step =>
|
||||||
|
sample.exerciseIndex == exercise &&
|
||||||
|
sample.setIndex == set &&
|
||||||
|
sample.stepIndex == step,
|
||||||
|
};
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
double? _scopeDelta(Iterable<double> values) {
|
||||||
|
final list = values.toList();
|
||||||
|
if (list.isEmpty) return null;
|
||||||
|
return math.max(0, list.last - list.first);
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _HeartRateTelemetryPainter extends CustomPainter {
|
||||||
|
const _HeartRateTelemetryPainter({
|
||||||
|
required this.samples,
|
||||||
|
required this.color,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<WorkoutTelemetrySample> samples;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final values = samples
|
||||||
|
.map((sample) => (at: sample.capturedAt, bpm: sample.heartRateBpm!))
|
||||||
|
.toList();
|
||||||
|
final minTime = values.first.at;
|
||||||
|
final maxTime = values.last.at.isAfter(minTime)
|
||||||
|
? values.last.at
|
||||||
|
: minTime.add(const Duration(seconds: 1));
|
||||||
|
final minBpm = values.map((value) => value.bpm).reduce(math.min);
|
||||||
|
final maxBpm = values.map((value) => value.bpm).reduce(math.max);
|
||||||
|
final chart = Rect.fromLTWH(0, 8, size.width, size.height - 16);
|
||||||
|
final range = math.max(1, maxBpm - minBpm);
|
||||||
|
Offset pointFor(({DateTime at, int bpm}) value) {
|
||||||
|
final xRatio =
|
||||||
|
value.at.difference(minTime).inMilliseconds /
|
||||||
|
maxTime.difference(minTime).inMilliseconds;
|
||||||
|
final yRatio = (value.bpm - minBpm) / range;
|
||||||
|
return Offset(
|
||||||
|
chart.left + chart.width * xRatio.clamp(0, 1),
|
||||||
|
chart.bottom - chart.height * yRatio.clamp(0, 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final paint = Paint()
|
||||||
|
..color = color
|
||||||
|
..strokeWidth = 2;
|
||||||
|
for (var index = 1; index < values.length; index += 1) {
|
||||||
|
final start = pointFor(values[index - 1]);
|
||||||
|
final end = pointFor(values[index]);
|
||||||
|
if (values[index].at.difference(values[index - 1].at) >
|
||||||
|
const Duration(minutes: 2)) {
|
||||||
|
_drawDashedLine(canvas, start, end, paint);
|
||||||
|
} else {
|
||||||
|
canvas.drawLine(start, end, paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant _HeartRateTelemetryPainter oldDelegate) =>
|
||||||
|
oldDelegate.samples != samples || oldDelegate.color != color;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint) {
|
||||||
|
const dash = 6.0;
|
||||||
|
const gap = 4.0;
|
||||||
|
final delta = end - start;
|
||||||
|
final distance = delta.distance;
|
||||||
|
if (distance == 0) return;
|
||||||
|
final direction = delta / distance;
|
||||||
|
var travelled = 0.0;
|
||||||
|
while (travelled < distance) {
|
||||||
|
final segmentEnd = math.min(travelled + dash, distance);
|
||||||
|
canvas.drawLine(
|
||||||
|
start + direction * travelled,
|
||||||
|
start + direction * segmentEnd,
|
||||||
|
paint,
|
||||||
|
);
|
||||||
|
travelled += dash + gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class _HistoryWatchStatsBars extends StatelessWidget {
|
final class _HistoryWatchStatsBars extends StatelessWidget {
|
||||||
const _HistoryWatchStatsBars({
|
const _HistoryWatchStatsBars({
|
||||||
required this.minHeartRateBpm,
|
required this.minHeartRateBpm,
|
||||||
|
|||||||
@ -162,6 +162,7 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
|||||||
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
||||||
stepUseCases: widget.bootstrap.activeExerciseStepUseCases,
|
stepUseCases: widget.bootstrap.activeExerciseStepUseCases,
|
||||||
sensorUseCases: widget.bootstrap.activeWorkoutSensorUseCases,
|
sensorUseCases: widget.bootstrap.activeWorkoutSensorUseCases,
|
||||||
|
telemetryUseCases: widget.bootstrap.workoutTelemetryUseCases,
|
||||||
performanceReferenceUseCase:
|
performanceReferenceUseCase:
|
||||||
widget.bootstrap.exercisePerformanceReferenceUseCase,
|
widget.bootstrap.exercisePerformanceReferenceUseCase,
|
||||||
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
||||||
@ -188,6 +189,7 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
|||||||
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
||||||
stepUseCases: widget.bootstrap.activeExerciseStepUseCases,
|
stepUseCases: widget.bootstrap.activeExerciseStepUseCases,
|
||||||
sensorUseCases: widget.bootstrap.activeWorkoutSensorUseCases,
|
sensorUseCases: widget.bootstrap.activeWorkoutSensorUseCases,
|
||||||
|
telemetryUseCases: widget.bootstrap.workoutTelemetryUseCases,
|
||||||
performanceReferenceUseCase:
|
performanceReferenceUseCase:
|
||||||
widget.bootstrap.exercisePerformanceReferenceUseCase,
|
widget.bootstrap.exercisePerformanceReferenceUseCase,
|
||||||
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
||||||
@ -273,6 +275,7 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
|||||||
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
||||||
stepUseCases: widget.bootstrap.activeExerciseStepUseCases,
|
stepUseCases: widget.bootstrap.activeExerciseStepUseCases,
|
||||||
sensorUseCases: widget.bootstrap.activeWorkoutSensorUseCases,
|
sensorUseCases: widget.bootstrap.activeWorkoutSensorUseCases,
|
||||||
|
telemetryUseCases: widget.bootstrap.workoutTelemetryUseCases,
|
||||||
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
||||||
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
|
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
|
||||||
workoutTemplateUseCases: widget.bootstrap.workoutTemplateUseCases,
|
workoutTemplateUseCases: widget.bootstrap.workoutTemplateUseCases,
|
||||||
|
|||||||
@ -30,6 +30,7 @@ final class WorkoutExecutionScreen extends StatefulWidget {
|
|||||||
this.videoMediaBuilder,
|
this.videoMediaBuilder,
|
||||||
this.stepUseCases,
|
this.stepUseCases,
|
||||||
this.sensorUseCases,
|
this.sensorUseCases,
|
||||||
|
this.telemetryUseCases,
|
||||||
this.stepAudioCuePlayer,
|
this.stepAudioCuePlayer,
|
||||||
this.watchAlertPublisher,
|
this.watchAlertPublisher,
|
||||||
super.key,
|
super.key,
|
||||||
@ -46,6 +47,7 @@ final class WorkoutExecutionScreen extends StatefulWidget {
|
|||||||
final Future<MediaAsset?> Function(String id)? mediaAssetLoader;
|
final Future<MediaAsset?> Function(String id)? mediaAssetLoader;
|
||||||
final VideoMediaBuilder? videoMediaBuilder;
|
final VideoMediaBuilder? videoMediaBuilder;
|
||||||
final ActiveWorkoutSensorUseCases? sensorUseCases;
|
final ActiveWorkoutSensorUseCases? sensorUseCases;
|
||||||
|
final WorkoutTelemetryUseCases? telemetryUseCases;
|
||||||
final ExerciseStepAudioCuePlayer? stepAudioCuePlayer;
|
final ExerciseStepAudioCuePlayer? stepAudioCuePlayer;
|
||||||
final WatchAlertPublisher? watchAlertPublisher;
|
final WatchAlertPublisher? watchAlertPublisher;
|
||||||
|
|
||||||
@ -274,11 +276,11 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_hasLiveSensorMetrics(_sensorState)) ...[
|
if (_shouldShowLiveSensorBar) ...[
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||||
child: _LiveSensorBar(
|
child: _LiveSensorBar(
|
||||||
sensorState: _sensorState!,
|
sensorState: _sensorState,
|
||||||
muted: _isSensorStateStale(_sensorState),
|
muted: _isSensorStateStale(_sensorState),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -405,10 +407,10 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const Text('Repos avant la prochaine série'),
|
const Text('Repos avant la prochaine série'),
|
||||||
if (_hasLiveSensorMetrics(_sensorState)) ...[
|
if (_shouldShowLiveSensorBar) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_LiveSensorBar(
|
_LiveSensorBar(
|
||||||
sensorState: _sensorState!,
|
sensorState: _sensorState,
|
||||||
muted: _isSensorStateStale(_sensorState),
|
muted: _isSensorStateStale(_sensorState),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -485,10 +487,10 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text('Pause', style: Theme.of(context).textTheme.headlineMedium),
|
Text('Pause', style: Theme.of(context).textTheme.headlineMedium),
|
||||||
if (_hasLiveSensorMetrics(_sensorState)) ...[
|
if (_shouldShowLiveSensorBar) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_LiveSensorBar(
|
_LiveSensorBar(
|
||||||
sensorState: _sensorState!,
|
sensorState: _sensorState,
|
||||||
muted: _isSensorStateStale(_sensorState),
|
muted: _isSensorStateStale(_sensorState),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -638,6 +640,8 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
return hasGlobalTimer && !_hasSetExecutionStarted;
|
return hasGlobalTimer && !_hasSetExecutionStarted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get _shouldShowLiveSensorBar => widget.sensorUseCases != null;
|
||||||
|
|
||||||
bool get _usesSetTimerForStopwatchScore {
|
bool get _usesSetTimerForStopwatchScore {
|
||||||
return _exercise.timeEnabled && _exercise.stopwatchScoreEnabled;
|
return _exercise.timeEnabled && _exercise.stopwatchScoreEnabled;
|
||||||
}
|
}
|
||||||
@ -1837,6 +1841,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
||||||
activeUseCases: widget.activeUseCases,
|
activeUseCases: widget.activeUseCases,
|
||||||
stepUseCases: widget.stepUseCases,
|
stepUseCases: widget.stepUseCases,
|
||||||
|
telemetryUseCases: widget.telemetryUseCases,
|
||||||
closeUseCase: widget.closeUseCase,
|
closeUseCase: widget.closeUseCase,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -1894,6 +1899,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
initialSession: session,
|
initialSession: session,
|
||||||
activeUseCases: widget.activeUseCases,
|
activeUseCases: widget.activeUseCases,
|
||||||
stepUseCases: widget.stepUseCases,
|
stepUseCases: widget.stepUseCases,
|
||||||
|
telemetryUseCases: widget.telemetryUseCases,
|
||||||
closeUseCase: widget.closeUseCase,
|
closeUseCase: widget.closeUseCase,
|
||||||
historyUseCases: widget.historyUseCases,
|
historyUseCases: widget.historyUseCases,
|
||||||
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
||||||
@ -2306,7 +2312,7 @@ final class _ExecutionAppBarTitle extends StatelessWidget {
|
|||||||
final class _LiveSensorBar extends StatelessWidget {
|
final class _LiveSensorBar extends StatelessWidget {
|
||||||
const _LiveSensorBar({required this.sensorState, required this.muted});
|
const _LiveSensorBar({required this.sensorState, required this.muted});
|
||||||
|
|
||||||
final ActiveWorkoutSensorState sensorState;
|
final ActiveWorkoutSensorState? sensorState;
|
||||||
final bool muted;
|
final bool muted;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -2318,23 +2324,20 @@ final class _LiveSensorBar extends StatelessWidget {
|
|||||||
runSpacing: 4,
|
runSpacing: 4,
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (sensorState.latestHeartRateBpm case final heartRate?)
|
|
||||||
_LiveSensorPill(
|
_LiveSensorPill(
|
||||||
icon: Icons.favorite,
|
icon: Icons.favorite,
|
||||||
label: 'FC $heartRate bpm',
|
label: _heartRateMetricLabel(sensorState),
|
||||||
muted: false,
|
muted: sensorState?.latestHeartRateBpm == null,
|
||||||
),
|
),
|
||||||
if (_distanceLabel(sensorState) case final distanceLabel?)
|
|
||||||
_LiveSensorPill(
|
_LiveSensorPill(
|
||||||
icon: Icons.directions_run,
|
icon: Icons.directions_run,
|
||||||
label: distanceLabel,
|
label: _distanceMetricLabel(sensorState),
|
||||||
muted: false,
|
muted: _distanceLabel(sensorState) == null,
|
||||||
),
|
),
|
||||||
if (_caloriesLabel(sensorState) case final caloriesLabel?)
|
|
||||||
_LiveSensorPill(
|
_LiveSensorPill(
|
||||||
icon: Icons.local_fire_department,
|
icon: Icons.local_fire_department,
|
||||||
label: caloriesLabel,
|
label: _caloriesMetricLabel(sensorState),
|
||||||
muted: false,
|
muted: _caloriesLabel(sensorState) == null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -5320,6 +5323,29 @@ bool _isSensorStateStale(ActiveWorkoutSensorState? state) {
|
|||||||
const Duration(seconds: 15);
|
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) {
|
String? _distanceLabel(ActiveWorkoutSensorState? state) {
|
||||||
final meters = state?.latestDistanceMeters;
|
final meters = state?.latestDistanceMeters;
|
||||||
if (meters == null || meters < 0) {
|
if (meters == null || meters < 0) {
|
||||||
@ -5339,13 +5365,6 @@ String? _caloriesLabel(ActiveWorkoutSensorState? state) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _hasLiveSensorMetrics(ActiveWorkoutSensorState? state) {
|
|
||||||
return state != null &&
|
|
||||||
(state.latestHeartRateBpm != null ||
|
|
||||||
state.latestDistanceMeters != null ||
|
|
||||||
_caloriesLabel(state) != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _formatStepCountdown(Duration duration) {
|
String _formatStepCountdown(Duration duration) {
|
||||||
final totalSeconds = (duration.inMilliseconds / 1000)
|
final totalSeconds = (duration.inMilliseconds / 1000)
|
||||||
.ceil()
|
.ceil()
|
||||||
|
|||||||
@ -20,6 +20,7 @@ final class WorkoutTemplateListScreen extends StatefulWidget {
|
|||||||
this.mediaUseCases,
|
this.mediaUseCases,
|
||||||
this.stepUseCases,
|
this.stepUseCases,
|
||||||
this.sensorUseCases,
|
this.sensorUseCases,
|
||||||
|
this.telemetryUseCases,
|
||||||
this.performanceReferenceUseCase,
|
this.performanceReferenceUseCase,
|
||||||
this.shareUseCases,
|
this.shareUseCases,
|
||||||
this.authUseCases,
|
this.authUseCases,
|
||||||
@ -33,6 +34,7 @@ final class WorkoutTemplateListScreen extends StatefulWidget {
|
|||||||
final ActiveWorkoutSessionUseCases activeUseCases;
|
final ActiveWorkoutSessionUseCases activeUseCases;
|
||||||
final ActiveExerciseStepUseCases? stepUseCases;
|
final ActiveExerciseStepUseCases? stepUseCases;
|
||||||
final ActiveWorkoutSensorUseCases? sensorUseCases;
|
final ActiveWorkoutSensorUseCases? sensorUseCases;
|
||||||
|
final WorkoutTelemetryUseCases? telemetryUseCases;
|
||||||
final ExercisePerformanceReferenceUseCase? performanceReferenceUseCase;
|
final ExercisePerformanceReferenceUseCase? performanceReferenceUseCase;
|
||||||
final CloseWorkoutSessionUseCase closeUseCase;
|
final CloseWorkoutSessionUseCase closeUseCase;
|
||||||
final WorkoutHistoryUseCases historyUseCases;
|
final WorkoutHistoryUseCases historyUseCases;
|
||||||
@ -313,6 +315,7 @@ final class _WorkoutTemplateListScreenState
|
|||||||
performanceReferenceUseCase: widget.performanceReferenceUseCase,
|
performanceReferenceUseCase: widget.performanceReferenceUseCase,
|
||||||
mediaUseCases: widget.mediaUseCases,
|
mediaUseCases: widget.mediaUseCases,
|
||||||
sensorUseCases: widget.sensorUseCases,
|
sensorUseCases: widget.sensorUseCases,
|
||||||
|
telemetryUseCases: widget.telemetryUseCases,
|
||||||
watchAlertPublisher: widget.watchAlertPublisher,
|
watchAlertPublisher: widget.watchAlertPublisher,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -128,6 +128,54 @@ void main() {
|
|||||||
expect(exerciseRepository.saved.single.tags, ['match']);
|
expect(exerciseRepository.saved.single.tags, ['match']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('choisir des types métier les sauvegarde avec exercice', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final exerciseRepository = _FakeExerciseRepository();
|
||||||
|
|
||||||
|
await tester.binding.setSurfaceSize(const Size(400, 1600));
|
||||||
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: ExerciseFormScreen(
|
||||||
|
exerciseUseCases: _exerciseUseCases(exerciseRepository),
|
||||||
|
mediaUseCases: _mediaUseCases(exerciseRepository),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Départ');
|
||||||
|
await tester.ensureVisible(find.text('Type d’exercice'));
|
||||||
|
await tester.dragUntilVisible(
|
||||||
|
find.widgetWithText(FilterChip, 'Dribble'),
|
||||||
|
find.byType(SingleChildScrollView).last,
|
||||||
|
const Offset(-180, 0),
|
||||||
|
);
|
||||||
|
await tester.tap(find.widgetWithText(FilterChip, 'Dribble'));
|
||||||
|
await tester.dragUntilVisible(
|
||||||
|
find.widgetWithText(FilterChip, 'Haute intensité'),
|
||||||
|
find.byType(SingleChildScrollView).last,
|
||||||
|
const Offset(-250, 0),
|
||||||
|
);
|
||||||
|
await tester.tap(find.widgetWithText(FilterChip, 'Haute intensité'));
|
||||||
|
await tester.ensureVisible(
|
||||||
|
find.widgetWithText(TextFormField, 'Durée par défaut (s)'),
|
||||||
|
);
|
||||||
|
await tester.enterText(
|
||||||
|
find.widgetWithText(TextFormField, 'Durée par défaut (s)'),
|
||||||
|
'30',
|
||||||
|
);
|
||||||
|
await tester.ensureVisible(find.text('Enregistrer'));
|
||||||
|
await tester.tap(find.text('Enregistrer'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(exerciseRepository.saved.single.businessTypes, [
|
||||||
|
BusinessExerciseType.dribble,
|
||||||
|
BusinessExerciseType.highIntensity,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('filtre les exercices par tag et efface tous les filtres', (
|
testWidgets('filtre les exercices par tag et efface tous les filtres', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
@ -414,7 +462,7 @@ void main() {
|
|||||||
await _pumpExerciseForm(tester, exerciseRepository);
|
await _pumpExerciseForm(tester, exerciseRepository);
|
||||||
await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Tirs');
|
await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Tirs');
|
||||||
await tester.enterText(
|
await tester.enterText(
|
||||||
find.widgetWithText(TextFormField, 'Temps par défaut (s)'),
|
find.widgetWithText(TextFormField, 'Durée par défaut (s)'),
|
||||||
'30',
|
'30',
|
||||||
);
|
);
|
||||||
await tester.tap(find.widgetWithText(SwitchListTile, 'Score'));
|
await tester.tap(find.widgetWithText(SwitchListTile, 'Score'));
|
||||||
@ -614,7 +662,7 @@ void main() {
|
|||||||
await _pumpExerciseForm(tester, exerciseRepository);
|
await _pumpExerciseForm(tester, exerciseRepository);
|
||||||
await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Combo');
|
await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Combo');
|
||||||
await tester.enterText(
|
await tester.enterText(
|
||||||
find.widgetWithText(TextFormField, 'Temps par défaut (s)'),
|
find.widgetWithText(TextFormField, 'Durée par défaut (s)'),
|
||||||
'30',
|
'30',
|
||||||
);
|
);
|
||||||
await tester.tap(
|
await tester.tap(
|
||||||
@ -655,7 +703,7 @@ void main() {
|
|||||||
await _pumpExerciseForm(tester, exerciseRepository);
|
await _pumpExerciseForm(tester, exerciseRepository);
|
||||||
await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Combo');
|
await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Combo');
|
||||||
await tester.enterText(
|
await tester.enterText(
|
||||||
find.widgetWithText(TextFormField, 'Temps par défaut (s)'),
|
find.widgetWithText(TextFormField, 'Durée par défaut (s)'),
|
||||||
'30',
|
'30',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -940,6 +940,19 @@ final class _FakeWorkoutTelemetryRepository
|
|||||||
return const [];
|
return const [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<WorkoutTelemetrySample>> listSamplesForScope({
|
||||||
|
required String sessionId,
|
||||||
|
required WorkoutTelemetryAggregateScope scope,
|
||||||
|
int? programIndex,
|
||||||
|
int? exerciseIndex,
|
||||||
|
int? setIndex,
|
||||||
|
int? passageIndex,
|
||||||
|
int? stepIndex,
|
||||||
|
}) async {
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> replaceAggregatesForSession({
|
Future<void> replaceAggregatesForSession({
|
||||||
required String sessionId,
|
required String sessionId,
|
||||||
|
|||||||
@ -237,7 +237,7 @@ void main() {
|
|||||||
expect(find.text('186 kcal'), findsOneWidget);
|
expect(find.text('186 kcal'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('n’affiche aucun indicateur capteur sans donnée FC', (
|
testWidgets('affiche les états capteur explicites avant la première mesure', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
final now = DateTime.now().toUtc();
|
final now = DateTime.now().toUtc();
|
||||||
@ -272,10 +272,10 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(find.textContaining('bpm'), findsNothing);
|
expect(find.text('Mesure en attente'), findsNWidgets(3));
|
||||||
expect(find.textContaining('kcal'), findsNothing);
|
expect(find.byIcon(Icons.favorite), findsOneWidget);
|
||||||
expect(find.byIcon(Icons.favorite), findsNothing);
|
expect(find.byIcon(Icons.directions_run), findsOneWidget);
|
||||||
expect(find.byIcon(Icons.local_fire_department), findsNothing);
|
expect(find.byIcon(Icons.local_fire_department), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('ne fabrique pas de calories sans mesure montre native', (
|
testWidgets('ne fabrique pas de calories sans mesure montre native', (
|
||||||
@ -331,6 +331,7 @@ void main() {
|
|||||||
|
|
||||||
expect(find.text('FC 124 bpm'), findsOneWidget);
|
expect(find.text('FC 124 bpm'), findsOneWidget);
|
||||||
expect(find.textContaining('kcal'), findsNothing);
|
expect(find.textContaining('kcal'), findsNothing);
|
||||||
|
expect(find.text('Donnée indisponible'), findsNWidgets(2));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('atténue la fréquence cardiaque périmée', (tester) async {
|
testWidgets('atténue la fréquence cardiaque périmée', (tester) async {
|
||||||
|
|||||||
Reference in New Issue
Block a user