feat(exercice): valeurs par défaut d'exercice (tickets #39/#40, parent #30)
Étend le modèle Drift (tables.dart, app_database.dart/.g.dart), les entités/use cases/repositories et les écrans bibliothèque d'exercices et programme pour supporter des valeurs par défaut par exercice (répétitions/temps/poids proposés à la création d'une série). flutter analyze propre, 51/51 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -287,6 +287,10 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
late final TextEditingController _descriptionController;
|
||||
late final TextEditingController _scoreLabelController;
|
||||
late final TextEditingController _scoreUnitController;
|
||||
late final TextEditingController _defaultTimeController;
|
||||
late final TextEditingController _defaultRepsController;
|
||||
late final TextEditingController _defaultScoreController;
|
||||
late final TextEditingController _defaultScoreTimeController;
|
||||
String? _imageMediaId;
|
||||
String? _videoMediaId;
|
||||
String? _selectedImageName;
|
||||
@ -311,6 +315,20 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
_descriptionController = TextEditingController(text: exercise?.description);
|
||||
_scoreLabelController = TextEditingController(text: exercise?.scoreLabel);
|
||||
_scoreUnitController = TextEditingController(text: exercise?.scoreUnit);
|
||||
_defaultTimeController = TextEditingController(
|
||||
text: _optionalIntText(exercise?.defaultTargetTimeSeconds),
|
||||
);
|
||||
_defaultRepsController = TextEditingController(
|
||||
text: _optionalIntText(exercise?.defaultTargetReps),
|
||||
);
|
||||
_defaultScoreController = TextEditingController(
|
||||
text: _optionalDoubleText(exercise?.defaultTargetScore),
|
||||
);
|
||||
_defaultScoreTimeController = TextEditingController(
|
||||
text: _optionalDoubleText(
|
||||
_millisecondsToSeconds(exercise?.defaultTargetScoreTimeMs),
|
||||
),
|
||||
);
|
||||
_imageMediaId = exercise?.imageMediaId;
|
||||
_videoMediaId = exercise?.videoMediaId;
|
||||
_hasTime = exercise?.hasTimeMeasure ?? true;
|
||||
@ -325,6 +343,10 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
_descriptionController.dispose();
|
||||
_scoreLabelController.dispose();
|
||||
_scoreUnitController.dispose();
|
||||
_defaultTimeController.dispose();
|
||||
_defaultRepsController.dispose();
|
||||
_defaultScoreController.dispose();
|
||||
_defaultScoreTimeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -389,12 +411,44 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
value: _hasTime,
|
||||
onChanged: (value) => _setMeasure(() => _hasTime = value),
|
||||
),
|
||||
if (_hasTime) ...[
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _defaultTimeController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Temps par défaut (s)',
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (value) => _hasTime
|
||||
? _positiveIntValidator(
|
||||
value,
|
||||
'Saisis un temps supérieur à 0.',
|
||||
)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
_MeasureSwitch(
|
||||
title: 'Répétitions',
|
||||
subtitle: 'Compter le nombre de mouvements réalisés.',
|
||||
value: _hasReps,
|
||||
onChanged: (value) => _setMeasure(() => _hasReps = value),
|
||||
),
|
||||
if (_hasReps) ...[
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _defaultRepsController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Répétitions par défaut',
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (value) => _hasReps
|
||||
? _positiveIntValidator(
|
||||
value,
|
||||
'Saisis un nombre de répétitions supérieur à 0.',
|
||||
)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
_MeasureSwitch(
|
||||
title: 'Score',
|
||||
subtitle: 'Saisir une valeur libre avec son unité.',
|
||||
@ -467,6 +521,49 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
: null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _defaultScoreController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Score par défaut',
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
validator: (value) {
|
||||
if (!_hasScore ||
|
||||
_scoreInputMode != ScoreInputMode.manual) {
|
||||
return null;
|
||||
}
|
||||
return _positiveDoubleValidator(
|
||||
value,
|
||||
'Saisis un score supérieur à 0.',
|
||||
);
|
||||
},
|
||||
),
|
||||
] else ...[
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _defaultScoreTimeController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Objectif de chrono par défaut (optionnel)',
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
validator: (value) {
|
||||
if (!_hasScore ||
|
||||
_scoreInputMode != ScoreInputMode.stopwatch ||
|
||||
value == null ||
|
||||
value.trim().isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return _positiveDoubleValidator(
|
||||
value,
|
||||
'Saisis un objectif supérieur à 0.',
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
if (_warning != null) ...[
|
||||
@ -586,6 +683,20 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
final scoreUnit = _hasScore && scoreInputMode == ScoreInputMode.manual
|
||||
? _scoreUnitController.text.trim()
|
||||
: null;
|
||||
final defaultTargetTimeSeconds = _hasTime
|
||||
? int.parse(_defaultTimeController.text.trim())
|
||||
: null;
|
||||
final defaultTargetReps = _hasReps
|
||||
? int.parse(_defaultRepsController.text.trim())
|
||||
: null;
|
||||
final defaultTargetScore =
|
||||
_hasScore && scoreInputMode == ScoreInputMode.manual
|
||||
? double.parse(_defaultScoreController.text.trim())
|
||||
: null;
|
||||
final defaultTargetScoreTimeMs =
|
||||
_hasScore && scoreInputMode == ScoreInputMode.stopwatch
|
||||
? _optionalSecondsToMilliseconds(_defaultScoreTimeController.text)
|
||||
: null;
|
||||
try {
|
||||
if (exercise == null) {
|
||||
await widget.exerciseUseCases.create(
|
||||
@ -599,6 +710,10 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
scoreInputMode: scoreInputMode,
|
||||
scoreLabel: scoreLabel,
|
||||
scoreUnit: scoreUnit,
|
||||
defaultTargetTimeSeconds: defaultTargetTimeSeconds,
|
||||
defaultTargetReps: defaultTargetReps,
|
||||
defaultTargetScore: defaultTargetScore,
|
||||
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
|
||||
);
|
||||
} else {
|
||||
await widget.exerciseUseCases.update(
|
||||
@ -613,6 +728,10 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
scoreInputMode: scoreInputMode,
|
||||
scoreLabel: scoreLabel,
|
||||
scoreUnit: scoreUnit,
|
||||
defaultTargetTimeSeconds: defaultTargetTimeSeconds,
|
||||
defaultTargetReps: defaultTargetReps,
|
||||
defaultTargetScore: defaultTargetScore,
|
||||
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
|
||||
);
|
||||
}
|
||||
if (mounted) {
|
||||
@ -642,6 +761,48 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
return text.isEmpty ? null : text;
|
||||
}
|
||||
|
||||
String? _positiveIntValidator(String? value, String message) {
|
||||
final number = int.tryParse(value?.trim() ?? '');
|
||||
if (number == null || number <= 0) {
|
||||
return message;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _positiveDoubleValidator(String? value, String message) {
|
||||
final number = double.tryParse(value?.trim() ?? '');
|
||||
if (number == null || number <= 0) {
|
||||
return message;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _optionalIntText(int? value) {
|
||||
return value == null ? '' : '$value';
|
||||
}
|
||||
|
||||
String _optionalDoubleText(double? value) {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
if (value == value.roundToDouble()) {
|
||||
return '${value.round()}';
|
||||
}
|
||||
return '$value';
|
||||
}
|
||||
|
||||
double? _millisecondsToSeconds(int? milliseconds) {
|
||||
return milliseconds == null ? null : milliseconds / 1000;
|
||||
}
|
||||
|
||||
int? _optionalSecondsToMilliseconds(String value) {
|
||||
final text = value.trim();
|
||||
if (text.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return (double.parse(text) * 1000).round();
|
||||
}
|
||||
|
||||
String _fileNameFromPath(String path) {
|
||||
final parts = path.split(RegExp(r'[/\\]'));
|
||||
return parts.isEmpty ? path : parts.last;
|
||||
|
||||
@ -223,7 +223,7 @@ final class _ProgramFormScreenState extends State<ProgramFormScreen> {
|
||||
buildDefaultDragHandles: false,
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
itemCount: _exercises.length,
|
||||
onReorder: _reorderExercise,
|
||||
onReorderItem: _reorderExercise,
|
||||
itemBuilder: (context, index) {
|
||||
final exercise = _exercises[index];
|
||||
return _ProgramExerciseCard(
|
||||
@ -517,7 +517,6 @@ final class _ProgramExerciseCustomizationScreen extends StatefulWidget {
|
||||
const _ProgramExerciseCustomizationScreen({
|
||||
required _ProgramExerciseDraft draft,
|
||||
required this.defaultRestSeconds,
|
||||
super.key,
|
||||
}) : _initialDraft = draft;
|
||||
|
||||
final _ProgramExerciseDraft _initialDraft;
|
||||
@ -853,6 +852,14 @@ final class _ProgramExerciseDraft {
|
||||
scoreUnitSnapshot: exercise.scoreUnit,
|
||||
setsCount: 3,
|
||||
enabledMeasures: exercise.availableMeasures,
|
||||
targetTimeSeconds: exercise.defaultTargetTimeSeconds,
|
||||
targetReps: exercise.defaultTargetReps,
|
||||
targetScore: exercise.scoreInputMode == ScoreInputMode.manual
|
||||
? exercise.defaultTargetScore
|
||||
: null,
|
||||
targetScoreTimeMs: exercise.scoreInputMode == ScoreInputMode.stopwatch
|
||||
? exercise.defaultTargetScoreTimeMs
|
||||
: null,
|
||||
restSecondsOverride: restSecondsOverride,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user