feat(ui): configuration du score chronométré (ticket #26)

Ajoute la configuration du score chronométré sur exercise_library_screen.dart
et program_screen.dart, s'appuyant sur les fondations domain du ticket
#25. flutter analyze propre, 41/41 tests verts, build APK debug validé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 09:08:16 +02:00
parent ab6b373a79
commit 2f732aa1b1
4 changed files with 299 additions and 38 deletions

View File

@ -246,6 +246,7 @@ final class ExerciseListTile extends StatelessWidget {
(measure) => MeasureBadge(
measure: measure,
scoreUnit: exercise.scoreUnit,
scoreInputMode: exercise.scoreInputMode,
),
),
if (hasMedia)
@ -293,6 +294,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
var _hasTime = true;
var _hasReps = false;
var _hasScore = false;
var _scoreInputMode = ScoreInputMode.manual;
var _saving = false;
var _importingImage = false;
var _importingVideo = false;
@ -314,6 +316,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
_hasTime = exercise?.hasTimeMeasure ?? true;
_hasReps = exercise?.hasRepsMeasure ?? false;
_hasScore = exercise?.hasScoreMeasure ?? false;
_scoreInputMode = exercise?.scoreInputMode ?? ScoreInputMode.manual;
}
@override
@ -408,27 +411,63 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
),
if (_hasScore) ...[
const SizedBox(height: 12),
TextFormField(
controller: _scoreLabelController,
decoration: const InputDecoration(labelText: 'Score à saisir'),
validator: (value) {
if (!_hasScore) return null;
return value == null || value.trim().isEmpty
? 'Le libellé du score est obligatoire.'
: null;
Text(
'Mode de saisie',
style: Theme.of(context).textTheme.titleSmall,
),
RadioListTile<ScoreInputMode>(
contentPadding: EdgeInsets.zero,
title: const Text('Saisie libre'),
value: ScoreInputMode.manual,
groupValue: _scoreInputMode,
onChanged: (value) {
if (value == null) return;
setState(() => _scoreInputMode = value);
},
),
const SizedBox(height: 12),
TextFormField(
controller: _scoreUnitController,
decoration: const InputDecoration(labelText: 'Unité'),
validator: (value) {
if (!_hasScore) return null;
return value == null || value.trim().isEmpty
? 'Lunité du score est obligatoire.'
: null;
RadioListTile<ScoreInputMode>(
contentPadding: EdgeInsets.zero,
title: const Text('Chrono intégré'),
subtitle: const Text('Temps réalisé'),
value: ScoreInputMode.stopwatch,
groupValue: _scoreInputMode,
onChanged: (value) {
if (value == null) return;
setState(() => _scoreInputMode = value);
},
),
if (_scoreInputMode == ScoreInputMode.manual) ...[
const SizedBox(height: 12),
TextFormField(
controller: _scoreLabelController,
decoration: const InputDecoration(
labelText: 'Score à saisir',
),
validator: (value) {
if (!_hasScore ||
_scoreInputMode != ScoreInputMode.manual) {
return null;
}
return value == null || value.trim().isEmpty
? 'Le libellé du score est obligatoire.'
: null;
},
),
const SizedBox(height: 12),
TextFormField(
controller: _scoreUnitController,
decoration: const InputDecoration(labelText: 'Unité'),
validator: (value) {
if (!_hasScore ||
_scoreInputMode != ScoreInputMode.manual) {
return null;
}
return value == null || value.trim().isEmpty
? 'Lunité du score est obligatoire.'
: null;
},
),
],
],
if (_warning != null) ...[
const SizedBox(height: 16),
@ -537,6 +576,16 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
}
setState(() => _saving = true);
final scoreInputMode = _hasScore ? _scoreInputMode : ScoreInputMode.manual;
final scoreLabel = _hasScore
? switch (scoreInputMode) {
ScoreInputMode.manual => _scoreLabelController.text.trim(),
ScoreInputMode.stopwatch => 'Temps réalisé',
}
: null;
final scoreUnit = _hasScore && scoreInputMode == ScoreInputMode.manual
? _scoreUnitController.text.trim()
: null;
try {
if (exercise == null) {
await widget.exerciseUseCases.create(
@ -547,8 +596,9 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
hasTimeMeasure: _hasTime,
hasRepsMeasure: _hasReps,
hasScoreMeasure: _hasScore,
scoreLabel: _hasScore ? _scoreLabelController.text.trim() : null,
scoreUnit: _hasScore ? _scoreUnitController.text.trim() : null,
scoreInputMode: scoreInputMode,
scoreLabel: scoreLabel,
scoreUnit: scoreUnit,
);
} else {
await widget.exerciseUseCases.update(
@ -560,12 +610,16 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
hasTimeMeasure: _hasTime,
hasRepsMeasure: _hasReps,
hasScoreMeasure: _hasScore,
scoreLabel: _hasScore ? _scoreLabelController.text.trim() : null,
scoreUnit: _hasScore ? _scoreUnitController.text.trim() : null,
scoreInputMode: scoreInputMode,
scoreLabel: scoreLabel,
scoreUnit: scoreUnit,
);
}
if (mounted) {
Navigator.of(context).pop(true);
final navigator = Navigator.of(context);
if (navigator.canPop()) {
navigator.pop(true);
}
}
} on Exception catch (error) {
_showSnackBar(error.toString());
@ -579,7 +633,8 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
bool _measuresChanged(Exercise exercise) {
return exercise.hasTimeMeasure != _hasTime ||
exercise.hasRepsMeasure != _hasReps ||
exercise.hasScoreMeasure != _hasScore;
exercise.hasScoreMeasure != _hasScore ||
exercise.scoreInputMode != _scoreInputMode;
}
String? _optionalText(TextEditingController controller) {
@ -601,20 +656,32 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
}
final class MeasureBadge extends StatelessWidget {
const MeasureBadge({required this.measure, this.scoreUnit, super.key});
const MeasureBadge({
required this.measure,
this.scoreInputMode = ScoreInputMode.manual,
this.scoreUnit,
super.key,
});
final WorkoutMeasure measure;
final ScoreInputMode scoreInputMode;
final String? scoreUnit;
@override
Widget build(BuildContext context) {
final unit = scoreUnit?.trim();
final label =
measure == WorkoutMeasure.score && unit != null && unit.isNotEmpty
? '${measure.label} ($unit)'
: measure.label;
var label = measure.label;
if (measure == WorkoutMeasure.score &&
scoreInputMode == ScoreInputMode.stopwatch) {
label = 'Score chrono';
} else if (measure == WorkoutMeasure.score &&
unit != null &&
unit.isNotEmpty) {
label = '${measure.label} ($unit)';
}
return Semantics(
label: label,
excludeSemantics: true,
child: Chip(visualDensity: VisualDensity.compact, label: Text(label)),
);
}