Ajoute la persistance de l'exécution par étapes avec résultats par passage (ports, use cases, entités, modèle Drift avec migration) et l'éditeur d'exercice avec séquence d'étapes sur exercise_library_screen.dart. Développés dans le même worktree partagé par DevBackend et DevFrontend ; commit combiné plutôt que séparé car les fakes de repository de test corrigés (nouvelles interfaces #58) touchent plusieurs écrans sans lien avec l'éditeur lui-même (historique, accueil, exécution, séance-modèle). Corrige au passage un ListTile sans ancêtre Material dans le nouvel éditeur d'étapes (Material(type: MaterialType.transparency)). flutter pub get OK, build_runner OK, analyze propre (mêmes infos préexistantes), 93/93 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
@ -400,7 +402,11 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
var _saving = false;
|
||||
var _importingImage = false;
|
||||
var _importingVideo = false;
|
||||
var _stepsEnabled = false;
|
||||
var _nextStepDraftId = 0;
|
||||
final _stepDrafts = <_ExerciseStepDraft>[];
|
||||
String? _measureError;
|
||||
String? _stepError;
|
||||
String? _warning;
|
||||
|
||||
bool get _isEditing => widget.exercise != null;
|
||||
@ -434,6 +440,10 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
_hasReps = exercise?.hasRepsMeasure ?? false;
|
||||
_hasScore = exercise?.hasScoreMeasure ?? false;
|
||||
_scoreInputMode = exercise?.scoreInputMode ?? ScoreInputMode.manual;
|
||||
_stepsEnabled = exercise?.steps.isNotEmpty ?? false;
|
||||
for (final step in exercise?.steps ?? const <ExerciseStep>[]) {
|
||||
_stepDrafts.add(_draftFromStep(step));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -446,6 +456,9 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
_defaultRepsController.dispose();
|
||||
_defaultScoreController.dispose();
|
||||
_defaultScoreTimeController.dispose();
|
||||
for (final draft in _stepDrafts) {
|
||||
draft.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -666,6 +679,8 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
),
|
||||
],
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
_buildExerciseStepsSection(context),
|
||||
if (_warning != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
MaterialBanner(
|
||||
@ -696,6 +711,305 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildExerciseStepsSection(BuildContext context) {
|
||||
final reachedLimit = _stepDrafts.length >= 8;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'Séquence d’étapes',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Rythmer cet exercice avec des étapes'),
|
||||
subtitle: Text(
|
||||
_stepsEnabled
|
||||
? 'Chaque passage suit ces étapes dans l’ordre. Si la série '
|
||||
'suit des répétitions, une répétition correspond à un '
|
||||
'passage complet.'
|
||||
: 'Ajoute des étapes si l’exercice doit suivre un ordre précis '
|
||||
'pendant chaque série.',
|
||||
),
|
||||
value: _stepsEnabled,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_stepsEnabled = value;
|
||||
_stepError = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (_stepsEnabled) ...[
|
||||
const SizedBox(height: 8),
|
||||
ReorderableListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
buildDefaultDragHandles: false,
|
||||
itemCount: _stepDrafts.length,
|
||||
onReorder: _reorderStep,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildStepCard(context, index, _stepDrafts[index]);
|
||||
},
|
||||
),
|
||||
if (_stepError != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_stepError!,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
if (reachedLimit)
|
||||
const ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(Icons.info_outline),
|
||||
title: Text('Limite atteinte'),
|
||||
subtitle: Text('Un exercice peut contenir jusqu’à 8 étapes.'),
|
||||
)
|
||||
else
|
||||
OutlinedButton.icon(
|
||||
onPressed: _addStep,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Ajouter une étape'),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStepCard(
|
||||
BuildContext context,
|
||||
int index,
|
||||
_ExerciseStepDraft draft,
|
||||
) {
|
||||
final targetLabel = draft.type == ExerciseStepType.time
|
||||
? 'Durée par défaut (s)'
|
||||
: 'Répétitions par défaut';
|
||||
final targetMessage = draft.type == ExerciseStepType.time
|
||||
? 'Saisis une durée supérieure à 0.'
|
||||
: 'Saisis un nombre de répétitions supérieur à 0.';
|
||||
final summaryUnit = draft.type == ExerciseStepType.time ? 's' : '';
|
||||
return CourtBlazerAccentPanel(
|
||||
key: ValueKey(draft.id),
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
ReorderableDragStartListener(
|
||||
index: index,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(right: 8),
|
||||
child: Icon(Icons.drag_handle),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${index + 1}. ${draft.displayName}',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
),
|
||||
PopupMenuButton<_StepAction>(
|
||||
tooltip: 'Actions de l’étape ${index + 1}',
|
||||
onSelected: (action) => _handleStepAction(index, action),
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: _StepAction.moveUp,
|
||||
enabled: index > 0,
|
||||
child: const Text('Monter'),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: _StepAction.moveDown,
|
||||
enabled: index < _stepDrafts.length - 1,
|
||||
child: const Text('Descendre'),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: _StepAction.duplicate,
|
||||
child: Text('Dupliquer'),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: _StepAction.delete,
|
||||
child: Text('Supprimer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
draft.type == ExerciseStepType.time
|
||||
? 'Temps · ${draft.targetController.text.trim()} $summaryUnit'
|
||||
: 'Répétitions · ${draft.targetController.text.trim()}',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: draft.nameController,
|
||||
decoration: const InputDecoration(labelText: 'Nom de l’étape'),
|
||||
validator: (value) {
|
||||
if (!_stepsEnabled) return null;
|
||||
return value == null || value.trim().isEmpty
|
||||
? 'Le nom de l’étape est obligatoire.'
|
||||
: null;
|
||||
},
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text('Type d’étape', style: Theme.of(context).textTheme.titleSmall),
|
||||
RadioListTile<ExerciseStepType>(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Temps'),
|
||||
value: ExerciseStepType.time,
|
||||
groupValue: draft.type,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
setState(() => draft.type = value);
|
||||
},
|
||||
),
|
||||
RadioListTile<ExerciseStepType>(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Répétitions'),
|
||||
value: ExerciseStepType.reps,
|
||||
groupValue: draft.type,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
setState(() => draft.type = value);
|
||||
},
|
||||
),
|
||||
TextFormField(
|
||||
controller: draft.targetController,
|
||||
decoration: InputDecoration(labelText: targetLabel),
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (value) =>
|
||||
_stepsEnabled ? _positiveIntValidator(value, targetMessage) : null,
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Score d’étape'),
|
||||
subtitle: const Text('Ajouter un score pour cette étape'),
|
||||
value: draft.hasScore,
|
||||
onChanged: (value) => setState(() => draft.hasScore = value),
|
||||
),
|
||||
if (draft.hasScore) ...[
|
||||
Text('Mode de score', style: Theme.of(context).textTheme.titleSmall),
|
||||
RadioListTile<ScoreInputMode>(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Saisie libre'),
|
||||
value: ScoreInputMode.manual,
|
||||
groupValue: draft.scoreInputMode,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
setState(() => draft.scoreInputMode = value);
|
||||
},
|
||||
),
|
||||
RadioListTile<ScoreInputMode>(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Chrono intégré'),
|
||||
value: ScoreInputMode.stopwatch,
|
||||
groupValue: draft.scoreInputMode,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
setState(() => draft.scoreInputMode = value);
|
||||
},
|
||||
),
|
||||
if (draft.scoreInputMode == ScoreInputMode.manual) ...[
|
||||
TextFormField(
|
||||
controller: draft.scoreLabelController,
|
||||
decoration: const InputDecoration(labelText: 'Score à saisir'),
|
||||
validator: (value) {
|
||||
if (!_stepsEnabled || !draft.hasScore) return null;
|
||||
if (draft.scoreInputMode != ScoreInputMode.manual) {
|
||||
return null;
|
||||
}
|
||||
return value == null || value.trim().isEmpty
|
||||
? 'Le libellé du score est obligatoire.'
|
||||
: null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: draft.scoreUnitController,
|
||||
decoration: const InputDecoration(labelText: 'Unité'),
|
||||
validator: (value) {
|
||||
if (!_stepsEnabled || !draft.hasScore) return null;
|
||||
if (draft.scoreInputMode != ScoreInputMode.manual) {
|
||||
return null;
|
||||
}
|
||||
return value == null || value.trim().isEmpty
|
||||
? 'L’unité du score est obligatoire.'
|
||||
: null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: draft.scoreTargetController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Score par défaut',
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
validator: (value) {
|
||||
if (!_stepsEnabled ||
|
||||
!draft.hasScore ||
|
||||
draft.scoreInputMode != ScoreInputMode.manual ||
|
||||
value == null ||
|
||||
value.trim().isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return _nonNegativeDoubleValidator(
|
||||
value,
|
||||
'Saisis un score supérieur ou égal à 0.',
|
||||
);
|
||||
},
|
||||
),
|
||||
] else ...[
|
||||
if (draft.type == ExerciseStepType.time) ...[
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Cette étape utilise déjà un compte à rebours. Le score '
|
||||
'chrono ajoute un second temps mesuré ; garde-le seulement '
|
||||
'si tu veux enregistrer une performance distincte.',
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: draft.scoreTargetController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Objectif de chrono par défaut (optionnel)',
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
validator: (value) {
|
||||
if (!_stepsEnabled ||
|
||||
!draft.hasScore ||
|
||||
draft.scoreInputMode != ScoreInputMode.stopwatch ||
|
||||
value == null ||
|
||||
value.trim().isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return _positiveDoubleValidator(
|
||||
value,
|
||||
'Saisis un objectif supérieur à 0.',
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _setMeasure(VoidCallback update) {
|
||||
setState(() {
|
||||
update();
|
||||
@ -704,6 +1018,141 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
void _addStep() {
|
||||
if (_stepDrafts.length >= 8) {
|
||||
setState(() {
|
||||
_stepError = 'Un exercice peut contenir jusqu’à 8 étapes.';
|
||||
});
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_stepError = null;
|
||||
_stepDrafts.add(
|
||||
_ExerciseStepDraft(
|
||||
id: 'step-draft-${_nextStepDraftId++}',
|
||||
name: 'Nouvelle étape',
|
||||
defaultTargetValue: '10',
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _reorderStep(int oldIndex, int newIndex) {
|
||||
setState(() {
|
||||
if (newIndex > oldIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final draft = _stepDrafts.removeAt(oldIndex);
|
||||
_stepDrafts.insert(newIndex, draft);
|
||||
});
|
||||
}
|
||||
|
||||
void _handleStepAction(int index, _StepAction action) {
|
||||
switch (action) {
|
||||
case _StepAction.moveUp:
|
||||
if (index <= 0) return;
|
||||
setState(() {
|
||||
final draft = _stepDrafts.removeAt(index);
|
||||
_stepDrafts.insert(index - 1, draft);
|
||||
});
|
||||
break;
|
||||
case _StepAction.moveDown:
|
||||
if (index >= _stepDrafts.length - 1) return;
|
||||
setState(() {
|
||||
final draft = _stepDrafts.removeAt(index);
|
||||
_stepDrafts.insert(index + 1, draft);
|
||||
});
|
||||
break;
|
||||
case _StepAction.duplicate:
|
||||
_duplicateStep(index);
|
||||
break;
|
||||
case _StepAction.delete:
|
||||
unawaited(_confirmRemoveStep(index));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _duplicateStep(int index) {
|
||||
if (_stepDrafts.length >= 8) {
|
||||
setState(() {
|
||||
_stepError = 'Un exercice peut contenir jusqu’à 8 étapes.';
|
||||
});
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_stepError = null;
|
||||
_stepDrafts.insert(index + 1, _stepDrafts[index].duplicate(
|
||||
'step-draft-${_nextStepDraftId++}',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _confirmRemoveStep(int index) async {
|
||||
final draft = _stepDrafts[index];
|
||||
var confirmed = true;
|
||||
if (draft.hasFilledContent) {
|
||||
confirmed =
|
||||
await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Supprimer cette étape ?'),
|
||||
content: const Text('Cette étape sera retirée de la séquence.'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('Supprimer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
if (!confirmed || !mounted) return;
|
||||
setState(() {
|
||||
final removed = _stepDrafts.removeAt(index);
|
||||
removed.dispose();
|
||||
_stepError = null;
|
||||
});
|
||||
}
|
||||
|
||||
_ExerciseStepDraft _draftFromStep(ExerciseStep step) {
|
||||
return _ExerciseStepDraft(
|
||||
id: step.id,
|
||||
name: step.name,
|
||||
type: step.type,
|
||||
defaultTargetValue: step.defaultTargetValue.toString(),
|
||||
hasScore: step.hasScore,
|
||||
scoreInputMode: step.scoreInputMode,
|
||||
scoreLabel: step.scoreLabel,
|
||||
scoreUnit: step.scoreUnit,
|
||||
scoreTarget: step.scoreInputMode == ScoreInputMode.stopwatch
|
||||
? _optionalDoubleText(
|
||||
_millisecondsToSeconds(step.defaultTargetScoreTimeMs),
|
||||
)
|
||||
: _optionalDoubleText(step.defaultTargetScore),
|
||||
);
|
||||
}
|
||||
|
||||
List<ExerciseStep> _buildSteps() {
|
||||
if (!_stepsEnabled) return const [];
|
||||
return [
|
||||
for (var index = 0; index < _stepDrafts.length; index++)
|
||||
_stepDrafts[index].toExerciseStep(
|
||||
position: index,
|
||||
defaultTargetScoreTimeMs: _stepDrafts[index].scoreInputMode ==
|
||||
ScoreInputMode.stopwatch
|
||||
? _optionalSecondsToMilliseconds(
|
||||
_stepDrafts[index].scoreTargetController.text,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> _importImage() async {
|
||||
if (_imageMediaIds.length >= 5) {
|
||||
_showSnackBar('Maximum 5 images par exercice.');
|
||||
@ -784,12 +1233,16 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
|
||||
Future<void> _save() async {
|
||||
final hasMeasure = _hasTime || _hasReps || _hasScore;
|
||||
final hasValidSteps = !_stepsEnabled || _stepDrafts.isNotEmpty;
|
||||
setState(() {
|
||||
_measureError = hasMeasure
|
||||
? null
|
||||
: 'Active au moins une mesure pour enregistrer.';
|
||||
_stepError = hasValidSteps
|
||||
? null
|
||||
: 'Ajoute au moins une étape ou désactive la séquence.';
|
||||
});
|
||||
if (!_formKey.currentState!.validate() || !hasMeasure) {
|
||||
if (!_formKey.currentState!.validate() || !hasMeasure || !hasValidSteps) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -832,6 +1285,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
_hasScore && scoreInputMode == ScoreInputMode.stopwatch
|
||||
? _optionalSecondsToMilliseconds(_defaultScoreTimeController.text)
|
||||
: null;
|
||||
final steps = _buildSteps();
|
||||
try {
|
||||
if (exercise == null) {
|
||||
await widget.exerciseUseCases.create(
|
||||
@ -850,6 +1304,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
defaultTargetReps: defaultTargetReps,
|
||||
defaultTargetScore: defaultTargetScore,
|
||||
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
|
||||
steps: steps,
|
||||
);
|
||||
} else {
|
||||
await widget.exerciseUseCases.update(
|
||||
@ -869,6 +1324,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
defaultTargetReps: defaultTargetReps,
|
||||
defaultTargetScore: defaultTargetScore,
|
||||
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
|
||||
steps: steps,
|
||||
);
|
||||
}
|
||||
if (mounted) {
|
||||
@ -961,6 +1417,104 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
enum _StepAction { moveUp, moveDown, duplicate, delete }
|
||||
|
||||
final class _ExerciseStepDraft {
|
||||
_ExerciseStepDraft({
|
||||
required this.id,
|
||||
required String name,
|
||||
this.type = ExerciseStepType.time,
|
||||
required String defaultTargetValue,
|
||||
this.hasScore = false,
|
||||
this.scoreInputMode = ScoreInputMode.manual,
|
||||
String? scoreLabel,
|
||||
String? scoreUnit,
|
||||
String? scoreTarget,
|
||||
}) : nameController = TextEditingController(text: name),
|
||||
targetController = TextEditingController(text: defaultTargetValue),
|
||||
scoreLabelController = TextEditingController(text: scoreLabel),
|
||||
scoreUnitController = TextEditingController(text: scoreUnit),
|
||||
scoreTargetController = TextEditingController(text: scoreTarget);
|
||||
|
||||
final String id;
|
||||
ExerciseStepType type;
|
||||
bool hasScore;
|
||||
ScoreInputMode scoreInputMode;
|
||||
final TextEditingController nameController;
|
||||
final TextEditingController targetController;
|
||||
final TextEditingController scoreLabelController;
|
||||
final TextEditingController scoreUnitController;
|
||||
final TextEditingController scoreTargetController;
|
||||
|
||||
String get displayName {
|
||||
final text = nameController.text.trim();
|
||||
return text.isEmpty ? 'Étape sans nom' : text;
|
||||
}
|
||||
|
||||
bool get hasFilledContent {
|
||||
return nameController.text.trim().isNotEmpty ||
|
||||
targetController.text.trim().isNotEmpty ||
|
||||
hasScore ||
|
||||
scoreLabelController.text.trim().isNotEmpty ||
|
||||
scoreUnitController.text.trim().isNotEmpty ||
|
||||
scoreTargetController.text.trim().isNotEmpty;
|
||||
}
|
||||
|
||||
_ExerciseStepDraft duplicate(String id) {
|
||||
return _ExerciseStepDraft(
|
||||
id: id,
|
||||
name: nameController.text,
|
||||
type: type,
|
||||
defaultTargetValue: targetController.text,
|
||||
hasScore: hasScore,
|
||||
scoreInputMode: scoreInputMode,
|
||||
scoreLabel: scoreLabelController.text,
|
||||
scoreUnit: scoreUnitController.text,
|
||||
scoreTarget: scoreTargetController.text,
|
||||
);
|
||||
}
|
||||
|
||||
ExerciseStep toExerciseStep({
|
||||
required int position,
|
||||
required int? defaultTargetScoreTimeMs,
|
||||
}) {
|
||||
final scoreTargetText = scoreTargetController.text.trim();
|
||||
return ExerciseStep(
|
||||
id: id,
|
||||
position: position,
|
||||
name: nameController.text.trim(),
|
||||
type: type,
|
||||
defaultTargetValue: int.parse(targetController.text.trim()),
|
||||
hasScore: hasScore,
|
||||
scoreInputMode: hasScore ? scoreInputMode : ScoreInputMode.manual,
|
||||
scoreLabel: hasScore && scoreInputMode == ScoreInputMode.manual
|
||||
? scoreLabelController.text.trim()
|
||||
: null,
|
||||
scoreUnit: hasScore && scoreInputMode == ScoreInputMode.manual
|
||||
? scoreUnitController.text.trim()
|
||||
: null,
|
||||
defaultTargetScore:
|
||||
hasScore &&
|
||||
scoreInputMode == ScoreInputMode.manual &&
|
||||
scoreTargetText.isNotEmpty
|
||||
? double.parse(scoreTargetText)
|
||||
: null,
|
||||
defaultTargetScoreTimeMs:
|
||||
hasScore && scoreInputMode == ScoreInputMode.stopwatch
|
||||
? defaultTargetScoreTimeMs
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
nameController.dispose();
|
||||
targetController.dispose();
|
||||
scoreLabelController.dispose();
|
||||
scoreUnitController.dispose();
|
||||
scoreTargetController.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
final class MeasureBadge extends StatelessWidget {
|
||||
const MeasureBadge({
|
||||
required this.measure,
|
||||
|
||||
Reference in New Issue
Block a user