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:
@ -246,6 +246,7 @@ final class ExerciseListTile extends StatelessWidget {
|
|||||||
(measure) => MeasureBadge(
|
(measure) => MeasureBadge(
|
||||||
measure: measure,
|
measure: measure,
|
||||||
scoreUnit: exercise.scoreUnit,
|
scoreUnit: exercise.scoreUnit,
|
||||||
|
scoreInputMode: exercise.scoreInputMode,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (hasMedia)
|
if (hasMedia)
|
||||||
@ -293,6 +294,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
var _hasTime = true;
|
var _hasTime = true;
|
||||||
var _hasReps = false;
|
var _hasReps = false;
|
||||||
var _hasScore = false;
|
var _hasScore = false;
|
||||||
|
var _scoreInputMode = ScoreInputMode.manual;
|
||||||
var _saving = false;
|
var _saving = false;
|
||||||
var _importingImage = false;
|
var _importingImage = false;
|
||||||
var _importingVideo = false;
|
var _importingVideo = false;
|
||||||
@ -314,6 +316,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
_hasTime = exercise?.hasTimeMeasure ?? true;
|
_hasTime = exercise?.hasTimeMeasure ?? true;
|
||||||
_hasReps = exercise?.hasRepsMeasure ?? false;
|
_hasReps = exercise?.hasRepsMeasure ?? false;
|
||||||
_hasScore = exercise?.hasScoreMeasure ?? false;
|
_hasScore = exercise?.hasScoreMeasure ?? false;
|
||||||
|
_scoreInputMode = exercise?.scoreInputMode ?? ScoreInputMode.manual;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -408,27 +411,63 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
),
|
),
|
||||||
if (_hasScore) ...[
|
if (_hasScore) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
TextFormField(
|
Text(
|
||||||
controller: _scoreLabelController,
|
'Mode de saisie',
|
||||||
decoration: const InputDecoration(labelText: 'Score à saisir'),
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
validator: (value) {
|
),
|
||||||
if (!_hasScore) return null;
|
RadioListTile<ScoreInputMode>(
|
||||||
return value == null || value.trim().isEmpty
|
contentPadding: EdgeInsets.zero,
|
||||||
? 'Le libellé du score est obligatoire.'
|
title: const Text('Saisie libre'),
|
||||||
: null;
|
value: ScoreInputMode.manual,
|
||||||
|
groupValue: _scoreInputMode,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == null) return;
|
||||||
|
setState(() => _scoreInputMode = value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
RadioListTile<ScoreInputMode>(
|
||||||
TextFormField(
|
contentPadding: EdgeInsets.zero,
|
||||||
controller: _scoreUnitController,
|
title: const Text('Chrono intégré'),
|
||||||
decoration: const InputDecoration(labelText: 'Unité'),
|
subtitle: const Text('Temps réalisé'),
|
||||||
validator: (value) {
|
value: ScoreInputMode.stopwatch,
|
||||||
if (!_hasScore) return null;
|
groupValue: _scoreInputMode,
|
||||||
return value == null || value.trim().isEmpty
|
onChanged: (value) {
|
||||||
? 'L’unité du score est obligatoire.'
|
if (value == null) return;
|
||||||
: null;
|
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
|
||||||
|
? 'L’unité du score est obligatoire.'
|
||||||
|
: null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
if (_warning != null) ...[
|
if (_warning != null) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@ -537,6 +576,16 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setState(() => _saving = true);
|
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 {
|
try {
|
||||||
if (exercise == null) {
|
if (exercise == null) {
|
||||||
await widget.exerciseUseCases.create(
|
await widget.exerciseUseCases.create(
|
||||||
@ -547,8 +596,9 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
hasTimeMeasure: _hasTime,
|
hasTimeMeasure: _hasTime,
|
||||||
hasRepsMeasure: _hasReps,
|
hasRepsMeasure: _hasReps,
|
||||||
hasScoreMeasure: _hasScore,
|
hasScoreMeasure: _hasScore,
|
||||||
scoreLabel: _hasScore ? _scoreLabelController.text.trim() : null,
|
scoreInputMode: scoreInputMode,
|
||||||
scoreUnit: _hasScore ? _scoreUnitController.text.trim() : null,
|
scoreLabel: scoreLabel,
|
||||||
|
scoreUnit: scoreUnit,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await widget.exerciseUseCases.update(
|
await widget.exerciseUseCases.update(
|
||||||
@ -560,12 +610,16 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
hasTimeMeasure: _hasTime,
|
hasTimeMeasure: _hasTime,
|
||||||
hasRepsMeasure: _hasReps,
|
hasRepsMeasure: _hasReps,
|
||||||
hasScoreMeasure: _hasScore,
|
hasScoreMeasure: _hasScore,
|
||||||
scoreLabel: _hasScore ? _scoreLabelController.text.trim() : null,
|
scoreInputMode: scoreInputMode,
|
||||||
scoreUnit: _hasScore ? _scoreUnitController.text.trim() : null,
|
scoreLabel: scoreLabel,
|
||||||
|
scoreUnit: scoreUnit,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.of(context).pop(true);
|
final navigator = Navigator.of(context);
|
||||||
|
if (navigator.canPop()) {
|
||||||
|
navigator.pop(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} on Exception catch (error) {
|
} on Exception catch (error) {
|
||||||
_showSnackBar(error.toString());
|
_showSnackBar(error.toString());
|
||||||
@ -579,7 +633,8 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
bool _measuresChanged(Exercise exercise) {
|
bool _measuresChanged(Exercise exercise) {
|
||||||
return exercise.hasTimeMeasure != _hasTime ||
|
return exercise.hasTimeMeasure != _hasTime ||
|
||||||
exercise.hasRepsMeasure != _hasReps ||
|
exercise.hasRepsMeasure != _hasReps ||
|
||||||
exercise.hasScoreMeasure != _hasScore;
|
exercise.hasScoreMeasure != _hasScore ||
|
||||||
|
exercise.scoreInputMode != _scoreInputMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _optionalText(TextEditingController controller) {
|
String? _optionalText(TextEditingController controller) {
|
||||||
@ -601,20 +656,32 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class MeasureBadge extends StatelessWidget {
|
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 WorkoutMeasure measure;
|
||||||
|
final ScoreInputMode scoreInputMode;
|
||||||
final String? scoreUnit;
|
final String? scoreUnit;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final unit = scoreUnit?.trim();
|
final unit = scoreUnit?.trim();
|
||||||
final label =
|
var label = measure.label;
|
||||||
measure == WorkoutMeasure.score && unit != null && unit.isNotEmpty
|
if (measure == WorkoutMeasure.score &&
|
||||||
? '${measure.label} ($unit)'
|
scoreInputMode == ScoreInputMode.stopwatch) {
|
||||||
: measure.label;
|
label = 'Score chrono';
|
||||||
|
} else if (measure == WorkoutMeasure.score &&
|
||||||
|
unit != null &&
|
||||||
|
unit.isNotEmpty) {
|
||||||
|
label = '${measure.label} ($unit)';
|
||||||
|
}
|
||||||
return Semantics(
|
return Semantics(
|
||||||
label: label,
|
label: label,
|
||||||
|
excludeSemantics: true,
|
||||||
child: Chip(visualDensity: VisualDensity.compact, label: Text(label)),
|
child: Chip(visualDensity: VisualDensity.compact, label: Text(label)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -539,6 +539,13 @@ final class _ProgramExerciseCustomizationScreenState
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final scoreUnit = _draft.scoreUnitSnapshot;
|
final scoreUnit = _draft.scoreUnitSnapshot;
|
||||||
|
final hasStopwatchScore =
|
||||||
|
_draft.scoreInputModeSnapshot == ScoreInputMode.stopwatch;
|
||||||
|
final scoreTitle = hasStopwatchScore
|
||||||
|
? 'Score chrono'
|
||||||
|
: scoreUnit == null
|
||||||
|
? 'Score'
|
||||||
|
: 'Score ($scoreUnit)';
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text("Personnaliser l'exercice")),
|
appBar: AppBar(title: const Text("Personnaliser l'exercice")),
|
||||||
bottomNavigationBar: SafeArea(
|
bottomNavigationBar: SafeArea(
|
||||||
@ -603,10 +610,19 @@ final class _ProgramExerciseCustomizationScreenState
|
|||||||
if (_draft.availableScoreSnapshot)
|
if (_draft.availableScoreSnapshot)
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
title: Text(scoreUnit == null ? 'Score' : 'Score ($scoreUnit)'),
|
title: Text(scoreTitle),
|
||||||
value: _draft.enabledMeasures.contains(WorkoutMeasure.score),
|
value: _draft.enabledMeasures.contains(WorkoutMeasure.score),
|
||||||
onChanged: (value) => _toggle(WorkoutMeasure.score, value),
|
onChanged: (value) => _toggle(WorkoutMeasure.score, value),
|
||||||
),
|
),
|
||||||
|
if (_draft.enabledMeasures.contains(WorkoutMeasure.time) &&
|
||||||
|
_draft.enabledMeasures.contains(WorkoutMeasure.score) &&
|
||||||
|
hasStopwatchScore) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
"Temps sert d'objectif de durée ; Score chrono enregistre "
|
||||||
|
'le temps réalisé.',
|
||||||
|
),
|
||||||
|
],
|
||||||
if (_measureError != null)
|
if (_measureError != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
@ -630,13 +646,31 @@ final class _ProgramExerciseCustomizationScreenState
|
|||||||
onChanged: (value) => _draft.targetReps = value?.round(),
|
onChanged: (value) => _draft.targetReps = value?.round(),
|
||||||
),
|
),
|
||||||
if (_draft.enabledMeasures.contains(WorkoutMeasure.score))
|
if (_draft.enabledMeasures.contains(WorkoutMeasure.score))
|
||||||
_NumberField(
|
if (hasStopwatchScore)
|
||||||
label: scoreUnit == null
|
_NumberField(
|
||||||
? 'Cible score'
|
label: 'Objectif de chrono',
|
||||||
: 'Cible score ($scoreUnit)',
|
helperText: 'Le résultat réel sera mesuré pendant la série.',
|
||||||
initialValue: _draft.targetScore,
|
initialValue: _millisecondsToSeconds(
|
||||||
onChanged: (value) => _draft.targetScore = value,
|
_draft.targetScoreTimeMs,
|
||||||
),
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
_draft.targetScore = null;
|
||||||
|
_draft.targetScoreTimeMs = value == null
|
||||||
|
? null
|
||||||
|
: (value * 1000).round();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else
|
||||||
|
_NumberField(
|
||||||
|
label: scoreUnit == null
|
||||||
|
? 'Cible score'
|
||||||
|
: 'Cible score ($scoreUnit)',
|
||||||
|
initialValue: _draft.targetScore,
|
||||||
|
onChanged: (value) {
|
||||||
|
_draft.targetScore = value;
|
||||||
|
_draft.targetScoreTimeMs = null;
|
||||||
|
},
|
||||||
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text('Repos', style: Theme.of(context).textTheme.titleMedium),
|
Text('Repos', style: Theme.of(context).textTheme.titleMedium),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@ -662,6 +696,10 @@ final class _ProgramExerciseCustomizationScreenState
|
|||||||
_draft.enabledMeasures = {..._draft.enabledMeasures, measure};
|
_draft.enabledMeasures = {..._draft.enabledMeasures, measure};
|
||||||
} else {
|
} else {
|
||||||
_draft.enabledMeasures = {..._draft.enabledMeasures}..remove(measure);
|
_draft.enabledMeasures = {..._draft.enabledMeasures}..remove(measure);
|
||||||
|
if (measure == WorkoutMeasure.score) {
|
||||||
|
_draft.targetScore = null;
|
||||||
|
_draft.targetScoreTimeMs = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -731,11 +769,13 @@ final class _NumberField extends StatelessWidget {
|
|||||||
required this.label,
|
required this.label,
|
||||||
required this.initialValue,
|
required this.initialValue,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
|
this.helperText,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
final num? initialValue;
|
final num? initialValue;
|
||||||
final ValueChanged<double?> onChanged;
|
final ValueChanged<double?> onChanged;
|
||||||
|
final String? helperText;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -743,7 +783,7 @@ final class _NumberField extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.only(top: 8),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
initialValue: initialValue?.toString(),
|
initialValue: initialValue?.toString(),
|
||||||
decoration: InputDecoration(labelText: label),
|
decoration: InputDecoration(labelText: label, helperText: helperText),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
onChanged: (value) => onChanged(double.tryParse(value)),
|
onChanged: (value) => onChanged(double.tryParse(value)),
|
||||||
),
|
),
|
||||||
@ -764,6 +804,7 @@ final class _ProgramExerciseDraft {
|
|||||||
required this.availableTimeSnapshot,
|
required this.availableTimeSnapshot,
|
||||||
required this.availableRepsSnapshot,
|
required this.availableRepsSnapshot,
|
||||||
required this.availableScoreSnapshot,
|
required this.availableScoreSnapshot,
|
||||||
|
this.scoreInputModeSnapshot = ScoreInputMode.manual,
|
||||||
this.scoreLabelSnapshot,
|
this.scoreLabelSnapshot,
|
||||||
this.scoreUnitSnapshot,
|
this.scoreUnitSnapshot,
|
||||||
required this.setsCount,
|
required this.setsCount,
|
||||||
@ -771,6 +812,7 @@ final class _ProgramExerciseDraft {
|
|||||||
this.targetTimeSeconds,
|
this.targetTimeSeconds,
|
||||||
this.targetReps,
|
this.targetReps,
|
||||||
this.targetScore,
|
this.targetScore,
|
||||||
|
this.targetScoreTimeMs,
|
||||||
this.restSecondsOverride,
|
this.restSecondsOverride,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -789,6 +831,7 @@ final class _ProgramExerciseDraft {
|
|||||||
availableTimeSnapshot: exercise.hasTimeMeasure,
|
availableTimeSnapshot: exercise.hasTimeMeasure,
|
||||||
availableRepsSnapshot: exercise.hasRepsMeasure,
|
availableRepsSnapshot: exercise.hasRepsMeasure,
|
||||||
availableScoreSnapshot: exercise.hasScoreMeasure,
|
availableScoreSnapshot: exercise.hasScoreMeasure,
|
||||||
|
scoreInputModeSnapshot: exercise.scoreInputMode,
|
||||||
scoreLabelSnapshot: exercise.scoreLabel,
|
scoreLabelSnapshot: exercise.scoreLabel,
|
||||||
scoreUnitSnapshot: exercise.scoreUnit,
|
scoreUnitSnapshot: exercise.scoreUnit,
|
||||||
setsCount: 3,
|
setsCount: 3,
|
||||||
@ -810,6 +853,7 @@ final class _ProgramExerciseDraft {
|
|||||||
availableTimeSnapshot: exercise.availableTimeSnapshot,
|
availableTimeSnapshot: exercise.availableTimeSnapshot,
|
||||||
availableRepsSnapshot: exercise.availableRepsSnapshot,
|
availableRepsSnapshot: exercise.availableRepsSnapshot,
|
||||||
availableScoreSnapshot: exercise.availableScoreSnapshot,
|
availableScoreSnapshot: exercise.availableScoreSnapshot,
|
||||||
|
scoreInputModeSnapshot: exercise.scoreInputModeSnapshot,
|
||||||
scoreLabelSnapshot: exercise.scoreLabelSnapshot,
|
scoreLabelSnapshot: exercise.scoreLabelSnapshot,
|
||||||
scoreUnitSnapshot: exercise.scoreUnitSnapshot,
|
scoreUnitSnapshot: exercise.scoreUnitSnapshot,
|
||||||
setsCount: exercise.setsCount,
|
setsCount: exercise.setsCount,
|
||||||
@ -821,6 +865,7 @@ final class _ProgramExerciseDraft {
|
|||||||
targetTimeSeconds: exercise.targetTimeSeconds,
|
targetTimeSeconds: exercise.targetTimeSeconds,
|
||||||
targetReps: exercise.targetReps,
|
targetReps: exercise.targetReps,
|
||||||
targetScore: exercise.targetScore,
|
targetScore: exercise.targetScore,
|
||||||
|
targetScoreTimeMs: exercise.targetScoreTimeMs,
|
||||||
restSecondsOverride: exercise.restSecondsOverride,
|
restSecondsOverride: exercise.restSecondsOverride,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -836,6 +881,7 @@ final class _ProgramExerciseDraft {
|
|||||||
final bool availableTimeSnapshot;
|
final bool availableTimeSnapshot;
|
||||||
final bool availableRepsSnapshot;
|
final bool availableRepsSnapshot;
|
||||||
final bool availableScoreSnapshot;
|
final bool availableScoreSnapshot;
|
||||||
|
final ScoreInputMode scoreInputModeSnapshot;
|
||||||
final String? scoreLabelSnapshot;
|
final String? scoreLabelSnapshot;
|
||||||
final String? scoreUnitSnapshot;
|
final String? scoreUnitSnapshot;
|
||||||
int setsCount;
|
int setsCount;
|
||||||
@ -843,6 +889,7 @@ final class _ProgramExerciseDraft {
|
|||||||
int? targetTimeSeconds;
|
int? targetTimeSeconds;
|
||||||
int? targetReps;
|
int? targetReps;
|
||||||
double? targetScore;
|
double? targetScore;
|
||||||
|
int? targetScoreTimeMs;
|
||||||
int? restSecondsOverride;
|
int? restSecondsOverride;
|
||||||
|
|
||||||
_ProgramExerciseDraft copy() {
|
_ProgramExerciseDraft copy() {
|
||||||
@ -858,6 +905,7 @@ final class _ProgramExerciseDraft {
|
|||||||
availableTimeSnapshot: availableTimeSnapshot,
|
availableTimeSnapshot: availableTimeSnapshot,
|
||||||
availableRepsSnapshot: availableRepsSnapshot,
|
availableRepsSnapshot: availableRepsSnapshot,
|
||||||
availableScoreSnapshot: availableScoreSnapshot,
|
availableScoreSnapshot: availableScoreSnapshot,
|
||||||
|
scoreInputModeSnapshot: scoreInputModeSnapshot,
|
||||||
scoreLabelSnapshot: scoreLabelSnapshot,
|
scoreLabelSnapshot: scoreLabelSnapshot,
|
||||||
scoreUnitSnapshot: scoreUnitSnapshot,
|
scoreUnitSnapshot: scoreUnitSnapshot,
|
||||||
setsCount: setsCount,
|
setsCount: setsCount,
|
||||||
@ -865,11 +913,14 @@ final class _ProgramExerciseDraft {
|
|||||||
targetTimeSeconds: targetTimeSeconds,
|
targetTimeSeconds: targetTimeSeconds,
|
||||||
targetReps: targetReps,
|
targetReps: targetReps,
|
||||||
targetScore: targetScore,
|
targetScore: targetScore,
|
||||||
|
targetScoreTimeMs: targetScoreTimeMs,
|
||||||
restSecondsOverride: restSecondsOverride,
|
restSecondsOverride: restSecondsOverride,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ProgramExerciseConfig toConfig() {
|
ProgramExerciseConfig toConfig() {
|
||||||
|
final scoreEnabled = enabledMeasures.contains(WorkoutMeasure.score);
|
||||||
|
final stopwatchScore = scoreInputModeSnapshot == ScoreInputMode.stopwatch;
|
||||||
return ProgramExerciseConfig(
|
return ProgramExerciseConfig(
|
||||||
existingMetadata: existingMetadata,
|
existingMetadata: existingMetadata,
|
||||||
sourceExerciseId: sourceExerciseId,
|
sourceExerciseId: sourceExerciseId,
|
||||||
@ -881,18 +932,26 @@ final class _ProgramExerciseDraft {
|
|||||||
availableTimeSnapshot: availableTimeSnapshot,
|
availableTimeSnapshot: availableTimeSnapshot,
|
||||||
availableRepsSnapshot: availableRepsSnapshot,
|
availableRepsSnapshot: availableRepsSnapshot,
|
||||||
availableScoreSnapshot: availableScoreSnapshot,
|
availableScoreSnapshot: availableScoreSnapshot,
|
||||||
|
scoreInputModeSnapshot: scoreInputModeSnapshot,
|
||||||
scoreLabelSnapshot: scoreLabelSnapshot,
|
scoreLabelSnapshot: scoreLabelSnapshot,
|
||||||
scoreUnitSnapshot: scoreUnitSnapshot,
|
scoreUnitSnapshot: scoreUnitSnapshot,
|
||||||
setsCount: setsCount,
|
setsCount: setsCount,
|
||||||
enabledMeasures: enabledMeasures,
|
enabledMeasures: enabledMeasures,
|
||||||
targetTimeSeconds: targetTimeSeconds,
|
targetTimeSeconds: targetTimeSeconds,
|
||||||
targetReps: targetReps,
|
targetReps: targetReps,
|
||||||
targetScore: targetScore,
|
targetScore: scoreEnabled && !stopwatchScore ? targetScore : null,
|
||||||
|
targetScoreTimeMs: scoreEnabled && stopwatchScore
|
||||||
|
? targetScoreTimeMs
|
||||||
|
: null,
|
||||||
restSecondsOverride: restSecondsOverride,
|
restSecondsOverride: restSecondsOverride,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double? _millisecondsToSeconds(int? milliseconds) {
|
||||||
|
return milliseconds == null ? null : milliseconds / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
String _programSummary(Program program) {
|
String _programSummary(Program program) {
|
||||||
final exerciseCount = program.exercises.length;
|
final exerciseCount = program.exercises.length;
|
||||||
final setsCount = program.exercises.fold<int>(
|
final setsCount = program.exercises.fold<int>(
|
||||||
@ -907,6 +966,10 @@ String _exerciseSummary(_ProgramExerciseDraft draft, int defaultRestSeconds) {
|
|||||||
final measures = draft.enabledMeasures
|
final measures = draft.enabledMeasures
|
||||||
.map((measure) {
|
.map((measure) {
|
||||||
final unit = draft.scoreUnitSnapshot?.trim();
|
final unit = draft.scoreUnitSnapshot?.trim();
|
||||||
|
if (measure == WorkoutMeasure.score &&
|
||||||
|
draft.scoreInputModeSnapshot == ScoreInputMode.stopwatch) {
|
||||||
|
return 'Score chrono';
|
||||||
|
}
|
||||||
if (measure == WorkoutMeasure.score &&
|
if (measure == WorkoutMeasure.score &&
|
||||||
unit != null &&
|
unit != null &&
|
||||||
unit.isNotEmpty) {
|
unit.isNotEmpty) {
|
||||||
|
|||||||
@ -66,6 +66,70 @@ void main() {
|
|||||||
expect(find.text('Score (kg)'), findsOneWidget);
|
expect(find.text('Score (kg)'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('basculer en chrono intégré masque l’unité et affiche le badge', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final exerciseRepository = _FakeExerciseRepository();
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: ExerciseFormScreen(
|
||||||
|
exerciseUseCases: _exerciseUseCases(exerciseRepository),
|
||||||
|
mediaUseCases: _mediaUseCases(exerciseRepository),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Run');
|
||||||
|
await tester.dragUntilVisible(
|
||||||
|
find.text('Score'),
|
||||||
|
find.byType(ListView),
|
||||||
|
const Offset(0, -200),
|
||||||
|
);
|
||||||
|
await tester.tap(find.widgetWithText(SwitchListTile, 'Score'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.dragUntilVisible(
|
||||||
|
find.text('Unité'),
|
||||||
|
find.byType(ListView),
|
||||||
|
const Offset(0, -200),
|
||||||
|
);
|
||||||
|
expect(find.widgetWithText(TextFormField, 'Unité'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.dragUntilVisible(
|
||||||
|
find.text('Chrono intégré'),
|
||||||
|
find.byType(ListView),
|
||||||
|
const Offset(0, 200),
|
||||||
|
);
|
||||||
|
await tester.tap(find.text('Chrono intégré'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.dragUntilVisible(
|
||||||
|
find.text('Temps réalisé'),
|
||||||
|
find.byType(ListView),
|
||||||
|
const Offset(0, -200),
|
||||||
|
);
|
||||||
|
expect(find.text('Temps réalisé'), findsOneWidget);
|
||||||
|
expect(find.widgetWithText(TextFormField, 'Unité'), findsNothing);
|
||||||
|
|
||||||
|
await tester.ensureVisible(find.text('Enregistrer'));
|
||||||
|
await tester.tap(find.text('Enregistrer'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final saved = exerciseRepository.saved.single;
|
||||||
|
expect(saved.scoreInputMode, ScoreInputMode.stopwatch);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(body: ExerciseListTile(exercise: saved)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Score chrono'), findsOneWidget);
|
||||||
|
expect(find.text('Score'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'sélectionner une image déclenche l’import et confirme le fichier',
|
'sélectionner une image déclenche l’import et confirme le fichier',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
|
|||||||
@ -102,6 +102,73 @@ void main() {
|
|||||||
expect(find.widgetWithText(SwitchListTile, 'Score'), findsNothing);
|
expect(find.widgetWithText(SwitchListTile, 'Score'), findsNothing);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets('affiche l’objectif de chrono pour un score en mode chrono', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final exerciseRepository = _FakeExerciseRepository()
|
||||||
|
..exercises.add(
|
||||||
|
Exercise(
|
||||||
|
metadata: _metadata('exercise-1'),
|
||||||
|
name: 'Suicides',
|
||||||
|
hasTimeMeasure: true,
|
||||||
|
hasRepsMeasure: true,
|
||||||
|
hasScoreMeasure: true,
|
||||||
|
scoreInputMode: ScoreInputMode.stopwatch,
|
||||||
|
scoreLabel: 'Temps réalisé',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: ProgramFormScreen(
|
||||||
|
programUseCases: _programUseCases(
|
||||||
|
_FakeProgramRepository(),
|
||||||
|
exerciseRepository,
|
||||||
|
),
|
||||||
|
exerciseUseCases: _exerciseUseCases(exerciseRepository),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Ajouter un exercice'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('Suicides'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byTooltip("Personnaliser l'exercice"));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Score chrono'), findsOneWidget);
|
||||||
|
await tester.dragUntilVisible(
|
||||||
|
find.text(
|
||||||
|
"Temps sert d'objectif de durée ; Score chrono enregistre "
|
||||||
|
'le temps réalisé.',
|
||||||
|
),
|
||||||
|
find.byType(ListView),
|
||||||
|
const Offset(0, -200),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
find.text(
|
||||||
|
"Temps sert d'objectif de durée ; Score chrono enregistre "
|
||||||
|
'le temps réalisé.',
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
await tester.dragUntilVisible(
|
||||||
|
find.text('Objectif de chrono'),
|
||||||
|
find.byType(ListView),
|
||||||
|
const Offset(0, -200),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
find.widgetWithText(TextFormField, 'Objectif de chrono'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(find.widgetWithText(TextFormField, 'Cible score'), findsNothing);
|
||||||
|
expect(
|
||||||
|
find.text('Le résultat réel sera mesuré pendant la série.'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ExerciseUseCases _exerciseUseCases(_FakeExerciseRepository repository) {
|
ExerciseUseCases _exerciseUseCases(_FakeExerciseRepository repository) {
|
||||||
|
|||||||
Reference in New Issue
Block a user