feat(ui): écran Composition de séance-modèle
Ajoute l'écran de composition de séance-modèle (presentation/workout_template_screen.dart), avec les ajustements application/domaine/repositories nécessaires et la navigation depuis l'accueil. flutter analyze propre, 12/12 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
722
lib/presentation/workout_template_screen.dart
Normal file
722
lib/presentation/workout_template_screen.dart
Normal file
@ -0,0 +1,722 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../application/application.dart';
|
||||
import '../domain/domain.dart';
|
||||
|
||||
final class WorkoutTemplateListScreen extends StatefulWidget {
|
||||
const WorkoutTemplateListScreen({
|
||||
required this.workoutTemplateUseCases,
|
||||
required this.programUseCases,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
||||
final ProgramUseCases programUseCases;
|
||||
|
||||
@override
|
||||
State<WorkoutTemplateListScreen> createState() =>
|
||||
_WorkoutTemplateListScreenState();
|
||||
}
|
||||
|
||||
final class _WorkoutTemplateListScreenState
|
||||
extends State<WorkoutTemplateListScreen> {
|
||||
late Future<List<WorkoutTemplate>> _templates;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_templates = widget.workoutTemplateUseCases.listActive();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Séances')),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => _openForm(),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Créer'),
|
||||
),
|
||||
body: FutureBuilder<List<WorkoutTemplate>>(
|
||||
future: _templates,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final templates = snapshot.data ?? const <WorkoutTemplate>[];
|
||||
if (templates.isEmpty) {
|
||||
return _CenteredMessage(
|
||||
title: 'Aucune séance pour le moment',
|
||||
message: 'Compose une séance à partir de tes programmes.',
|
||||
actionLabel: 'Créer une séance',
|
||||
onAction: () => _openForm(),
|
||||
);
|
||||
}
|
||||
return ListView.separated(
|
||||
itemCount: templates.length,
|
||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final template = templates[index];
|
||||
return ListTile(
|
||||
title: Text(template.name),
|
||||
subtitle: Text(_templateSummary(template)),
|
||||
trailing: Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
TextButton(onPressed: () {}, child: const Text('Lancer')),
|
||||
const Icon(Icons.chevron_right),
|
||||
],
|
||||
),
|
||||
onTap: () => _openForm(template: template),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _reload() {
|
||||
setState(() {
|
||||
_templates = widget.workoutTemplateUseCases.listActive();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _openForm({WorkoutTemplate? template}) async {
|
||||
final changed = await Navigator.of(context).push<bool>(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => WorkoutTemplateFormScreen(
|
||||
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
||||
programUseCases: widget.programUseCases,
|
||||
template: template,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (changed == true) {
|
||||
if (!mounted) return;
|
||||
_reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class WorkoutTemplateFormScreen extends StatefulWidget {
|
||||
const WorkoutTemplateFormScreen({
|
||||
required this.workoutTemplateUseCases,
|
||||
required this.programUseCases,
|
||||
this.template,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
||||
final ProgramUseCases programUseCases;
|
||||
final WorkoutTemplate? template;
|
||||
|
||||
@override
|
||||
State<WorkoutTemplateFormScreen> createState() =>
|
||||
_WorkoutTemplateFormScreenState();
|
||||
}
|
||||
|
||||
final class _WorkoutTemplateFormScreenState
|
||||
extends State<WorkoutTemplateFormScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late final TextEditingController _nameController;
|
||||
late final List<WorkoutTemplateProgramDraft> _programs;
|
||||
var _saving = false;
|
||||
|
||||
bool get _isEditing => widget.template != null;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final template = widget.template;
|
||||
_nameController = TextEditingController(text: template?.name);
|
||||
_programs = [
|
||||
for (final program
|
||||
in template?.programs ?? const <WorkoutTemplateProgram>[])
|
||||
WorkoutTemplateProgramDraft.fromTemplateProgram(
|
||||
program,
|
||||
template?.overrides ?? const <WorkoutTemplateExerciseOverride>[],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_isEditing ? 'Modifier la séance' : 'Créer une séance'),
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
minimum: const EdgeInsets.all(16),
|
||||
child: FilledButton.icon(
|
||||
onPressed: _saving ? null : _save,
|
||||
icon: _saving
|
||||
? const SizedBox.square(
|
||||
dimension: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.check),
|
||||
label: const Text('Enregistrer'),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) => value == null || value.trim().isEmpty
|
||||
? 'Le nom est obligatoire.'
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _addProgram,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Ajouter un programme'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _programs.isEmpty
|
||||
? const _CenteredMessage(
|
||||
title: 'Aucun programme ajouté',
|
||||
message: 'Ajoute un programme pour composer la séance.',
|
||||
)
|
||||
: ReorderableListView.builder(
|
||||
buildDefaultDragHandles: false,
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
itemCount: _programs.length,
|
||||
onReorder: _reorderProgram,
|
||||
itemBuilder: (context, index) {
|
||||
final program = _programs[index];
|
||||
return Card(
|
||||
key: ValueKey(program.clientKey),
|
||||
child: ListTile(
|
||||
leading: ReorderableDragStartListener(
|
||||
index: index,
|
||||
child: const Icon(Icons.drag_handle),
|
||||
),
|
||||
title: Text(program.programNameSnapshot),
|
||||
subtitle: Text(_programDraftSummary(program)),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _openProgramDetail(program),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _addProgram() async {
|
||||
final program = await Navigator.of(context).push<Program>(
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
ProgramSelectionScreen(programUseCases: widget.programUseCases),
|
||||
),
|
||||
);
|
||||
if (program == null) return;
|
||||
if (!mounted) return;
|
||||
final shouldExplainSnapshot = _programs.isEmpty;
|
||||
setState(() {
|
||||
_programs.add(WorkoutTemplateProgramDraft.fromProgram(program));
|
||||
});
|
||||
if (shouldExplainSnapshot) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Une copie de ce programme sera enregistrée dans la séance. '
|
||||
'Les futures modifications du programme original ne changeront '
|
||||
'pas cette séance.',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _reorderProgram(int oldIndex, int newIndex) {
|
||||
setState(() {
|
||||
if (newIndex > oldIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final item = _programs.removeAt(oldIndex);
|
||||
_programs.insert(newIndex, item);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _openProgramDetail(WorkoutTemplateProgramDraft program) async {
|
||||
await Navigator.of(context).push<void>(
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
WorkoutTemplateProgramDetailScreen(program: program),
|
||||
),
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
setState(() => _saving = true);
|
||||
try {
|
||||
await widget.workoutTemplateUseCases.saveConfigured(
|
||||
id: widget.template?.metadata.id,
|
||||
name: _nameController.text.trim(),
|
||||
programs: _programs.map((program) => program.toConfig()).toList(),
|
||||
overrides: _programs
|
||||
.expand((program) => program.toOverrides())
|
||||
.toList(),
|
||||
);
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
} on Exception catch (error) {
|
||||
_showSnackBar(error.toString());
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _saving = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showSnackBar(String message) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(message)));
|
||||
}
|
||||
}
|
||||
|
||||
final class ProgramSelectionScreen extends StatelessWidget {
|
||||
const ProgramSelectionScreen({required this.programUseCases, super.key});
|
||||
|
||||
final ProgramUseCases programUseCases;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Ajouter un programme')),
|
||||
body: FutureBuilder<List<Program>>(
|
||||
future: programUseCases.listActive(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final programs = snapshot.data ?? const <Program>[];
|
||||
if (programs.isEmpty) {
|
||||
return const _CenteredMessage(
|
||||
title: 'Aucun programme disponible',
|
||||
message: 'Crée un programme avant de composer une séance.',
|
||||
);
|
||||
}
|
||||
return ListView.separated(
|
||||
itemCount: programs.length,
|
||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final program = programs[index];
|
||||
return ListTile(
|
||||
title: Text(program.name),
|
||||
subtitle: Text(_programSummary(program)),
|
||||
onTap: () => Navigator.of(context).pop(program),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class WorkoutTemplateProgramDetailScreen extends StatefulWidget {
|
||||
const WorkoutTemplateProgramDetailScreen({required this.program, super.key});
|
||||
|
||||
final WorkoutTemplateProgramDraft program;
|
||||
|
||||
@override
|
||||
State<WorkoutTemplateProgramDetailScreen> createState() =>
|
||||
_WorkoutTemplateProgramDetailScreenState();
|
||||
}
|
||||
|
||||
final class _WorkoutTemplateProgramDetailScreenState
|
||||
extends State<WorkoutTemplateProgramDetailScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(widget.program.programNameSnapshot)),
|
||||
body: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: widget.program.exercises.length,
|
||||
itemBuilder: (context, index) {
|
||||
final exercise = widget.program.exercises[index];
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
exercise.exerciseNameSnapshot,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(_exerciseDraftSummary(exercise)),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
initialValue:
|
||||
'${exercise.setsCountOverride ?? exercise.setsCount}',
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nombre de séries',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
exercise.setsCountOverride = int.tryParse(value);
|
||||
});
|
||||
},
|
||||
),
|
||||
if (exercise.timeEnabled)
|
||||
_NumberField(
|
||||
label: 'Cible temps (s)',
|
||||
initialValue:
|
||||
exercise.targetTimeSecondsOverride ??
|
||||
exercise.targetTimeSeconds,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
exercise.targetTimeSecondsOverride = value?.round();
|
||||
});
|
||||
},
|
||||
),
|
||||
if (exercise.repsEnabled)
|
||||
_NumberField(
|
||||
label: 'Cible répétitions',
|
||||
initialValue:
|
||||
exercise.targetRepsOverride ?? exercise.targetReps,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
exercise.targetRepsOverride = value?.round();
|
||||
});
|
||||
},
|
||||
),
|
||||
if (exercise.scoreEnabled)
|
||||
_NumberField(
|
||||
label: exercise.scoreUnitSnapshot == null
|
||||
? 'Cible score'
|
||||
: 'Cible score (${exercise.scoreUnitSnapshot})',
|
||||
initialValue:
|
||||
exercise.targetScoreOverride ?? exercise.targetScore,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
exercise.targetScoreOverride = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class WorkoutTemplateProgramDraft {
|
||||
WorkoutTemplateProgramDraft({
|
||||
required this.clientKey,
|
||||
this.existingMetadata,
|
||||
this.sourceProgramId,
|
||||
required this.programNameSnapshot,
|
||||
required this.defaultRestSecondsSnapshot,
|
||||
required this.programSnapshotJson,
|
||||
required this.exercises,
|
||||
});
|
||||
|
||||
factory WorkoutTemplateProgramDraft.fromProgram(Program program) {
|
||||
final snapshot = {
|
||||
'programId': program.metadata.id,
|
||||
'name': program.name,
|
||||
'defaultRestSeconds': program.defaultRestSeconds,
|
||||
'exercises': program.exercises
|
||||
.map((exercise) => exercise.toSnapshotJson())
|
||||
.toList(),
|
||||
};
|
||||
return WorkoutTemplateProgramDraft(
|
||||
clientKey: UniqueKey().toString(),
|
||||
sourceProgramId: program.metadata.id,
|
||||
programNameSnapshot: program.name,
|
||||
defaultRestSecondsSnapshot: program.defaultRestSeconds,
|
||||
programSnapshotJson: jsonEncode(snapshot),
|
||||
exercises: _parseExercises(snapshot),
|
||||
);
|
||||
}
|
||||
|
||||
factory WorkoutTemplateProgramDraft.fromTemplateProgram(
|
||||
WorkoutTemplateProgram program,
|
||||
List<WorkoutTemplateExerciseOverride> overrides,
|
||||
) {
|
||||
final snapshot =
|
||||
jsonDecode(program.programSnapshotJson) as Map<String, dynamic>;
|
||||
final exercises = _parseExercises(snapshot);
|
||||
for (final exercise in exercises) {
|
||||
for (final override in overrides) {
|
||||
if (override.workoutTemplateProgramId == program.metadata.id &&
|
||||
override.snapshotProgramExerciseId == exercise.snapshotExerciseId) {
|
||||
exercise.existingOverrideMetadata = override.metadata;
|
||||
exercise.setsCountOverride = override.setsCountOverride;
|
||||
exercise.targetTimeSecondsOverride =
|
||||
override.targetTimeSecondsOverride;
|
||||
exercise.targetRepsOverride = override.targetRepsOverride;
|
||||
exercise.targetScoreOverride = override.targetScoreOverride;
|
||||
}
|
||||
}
|
||||
}
|
||||
return WorkoutTemplateProgramDraft(
|
||||
clientKey: program.metadata.id,
|
||||
existingMetadata: program.metadata,
|
||||
sourceProgramId: program.sourceProgramId,
|
||||
programNameSnapshot: program.programNameSnapshot,
|
||||
defaultRestSecondsSnapshot: program.defaultRestSecondsSnapshot,
|
||||
programSnapshotJson: program.programSnapshotJson,
|
||||
exercises: exercises,
|
||||
);
|
||||
}
|
||||
|
||||
final String clientKey;
|
||||
final EntityMetadata? existingMetadata;
|
||||
final String? sourceProgramId;
|
||||
final String programNameSnapshot;
|
||||
final int defaultRestSecondsSnapshot;
|
||||
final String programSnapshotJson;
|
||||
final List<WorkoutTemplateExerciseDraft> exercises;
|
||||
|
||||
WorkoutTemplateProgramConfig toConfig() {
|
||||
return WorkoutTemplateProgramConfig(
|
||||
clientKey: clientKey,
|
||||
existingMetadata: existingMetadata,
|
||||
sourceProgramId: sourceProgramId,
|
||||
programNameSnapshot: programNameSnapshot,
|
||||
defaultRestSecondsSnapshot: defaultRestSecondsSnapshot,
|
||||
programSnapshotJson: programSnapshotJson,
|
||||
);
|
||||
}
|
||||
|
||||
Iterable<WorkoutTemplateExerciseOverrideConfig> toOverrides() sync* {
|
||||
for (final exercise in exercises) {
|
||||
if (!exercise.hasOverride) {
|
||||
continue;
|
||||
}
|
||||
yield WorkoutTemplateExerciseOverrideConfig(
|
||||
existingMetadata: exercise.existingOverrideMetadata,
|
||||
workoutTemplateProgramClientKey: clientKey,
|
||||
snapshotProgramExerciseId: exercise.snapshotExerciseId,
|
||||
setsCountOverride: exercise.setsCountOverride,
|
||||
targetTimeSecondsOverride: exercise.targetTimeSecondsOverride,
|
||||
targetRepsOverride: exercise.targetRepsOverride,
|
||||
targetScoreOverride: exercise.targetScoreOverride,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class WorkoutTemplateExerciseDraft {
|
||||
WorkoutTemplateExerciseDraft({
|
||||
required this.snapshotExerciseId,
|
||||
required this.exerciseNameSnapshot,
|
||||
required this.setsCount,
|
||||
required this.timeEnabled,
|
||||
required this.repsEnabled,
|
||||
required this.scoreEnabled,
|
||||
this.scoreUnitSnapshot,
|
||||
this.targetTimeSeconds,
|
||||
this.targetReps,
|
||||
this.targetScore,
|
||||
this.existingOverrideMetadata,
|
||||
this.setsCountOverride,
|
||||
this.targetTimeSecondsOverride,
|
||||
this.targetRepsOverride,
|
||||
this.targetScoreOverride,
|
||||
});
|
||||
|
||||
final String snapshotExerciseId;
|
||||
final String exerciseNameSnapshot;
|
||||
final int setsCount;
|
||||
final bool timeEnabled;
|
||||
final bool repsEnabled;
|
||||
final bool scoreEnabled;
|
||||
final String? scoreUnitSnapshot;
|
||||
final int? targetTimeSeconds;
|
||||
final int? targetReps;
|
||||
final double? targetScore;
|
||||
EntityMetadata? existingOverrideMetadata;
|
||||
int? setsCountOverride;
|
||||
int? targetTimeSecondsOverride;
|
||||
int? targetRepsOverride;
|
||||
double? targetScoreOverride;
|
||||
|
||||
bool get hasOverride {
|
||||
return setsCountOverride != null ||
|
||||
targetTimeSecondsOverride != null ||
|
||||
targetRepsOverride != null ||
|
||||
targetScoreOverride != null;
|
||||
}
|
||||
}
|
||||
|
||||
final class _NumberField extends StatelessWidget {
|
||||
const _NumberField({
|
||||
required this.label,
|
||||
required this.initialValue,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final num? initialValue;
|
||||
final ValueChanged<double?> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: TextFormField(
|
||||
initialValue: initialValue?.toString(),
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (value) => onChanged(double.tryParse(value)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<WorkoutTemplateExerciseDraft> _parseExercises(
|
||||
Map<String, dynamic> snapshot,
|
||||
) {
|
||||
final rawExercises = snapshot['exercises'] as List<dynamic>? ?? const [];
|
||||
return rawExercises.map((raw) {
|
||||
final exercise = raw as Map<String, dynamic>;
|
||||
return WorkoutTemplateExerciseDraft(
|
||||
snapshotExerciseId: exercise['id'] as String,
|
||||
exerciseNameSnapshot: exercise['exerciseNameSnapshot'] as String,
|
||||
setsCount: exercise['setsCount'] as int,
|
||||
timeEnabled: exercise['timeEnabled'] as bool,
|
||||
repsEnabled: exercise['repsEnabled'] as bool,
|
||||
scoreEnabled: exercise['scoreEnabled'] as bool,
|
||||
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
|
||||
targetTimeSeconds: exercise['targetTimeSeconds'] as int?,
|
||||
targetReps: exercise['targetReps'] as int?,
|
||||
targetScore: (exercise['targetScore'] as num?)?.toDouble(),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
String _templateSummary(WorkoutTemplate template) {
|
||||
final programCount = template.programs.length;
|
||||
final exerciseCount = template.programs.fold<int>(
|
||||
0,
|
||||
(total, program) => total + _exerciseCount(program.programSnapshotJson),
|
||||
);
|
||||
return '$programCount programme${programCount > 1 ? 's' : ''} · '
|
||||
'$exerciseCount exercice${exerciseCount > 1 ? 's' : ''}';
|
||||
}
|
||||
|
||||
String _programDraftSummary(WorkoutTemplateProgramDraft program) {
|
||||
final exerciseCount = program.exercises.length;
|
||||
return '$exerciseCount exercice${exerciseCount > 1 ? 's' : ''}';
|
||||
}
|
||||
|
||||
String _programSummary(Program program) {
|
||||
final exerciseCount = program.exercises.length;
|
||||
final setsCount = program.exercises.fold<int>(
|
||||
0,
|
||||
(total, exercise) => total + exercise.setsCount,
|
||||
);
|
||||
return '$exerciseCount exercice${exerciseCount > 1 ? 's' : ''} · '
|
||||
'$setsCount série${setsCount > 1 ? 's' : ''}';
|
||||
}
|
||||
|
||||
String _exerciseDraftSummary(WorkoutTemplateExerciseDraft exercise) {
|
||||
final measures = [
|
||||
if (exercise.timeEnabled) 'Temps',
|
||||
if (exercise.repsEnabled) 'Répétitions',
|
||||
if (exercise.scoreEnabled)
|
||||
exercise.scoreUnitSnapshot == null
|
||||
? 'Score'
|
||||
: 'Score (${exercise.scoreUnitSnapshot})',
|
||||
].join(' + ');
|
||||
final sets = exercise.setsCountOverride ?? exercise.setsCount;
|
||||
return '$sets séries · $measures';
|
||||
}
|
||||
|
||||
int _exerciseCount(String programSnapshotJson) {
|
||||
final snapshot = jsonDecode(programSnapshotJson) as Map<String, dynamic>;
|
||||
return (snapshot['exercises'] as List<dynamic>? ?? const []).length;
|
||||
}
|
||||
|
||||
final class _CenteredMessage extends StatelessWidget {
|
||||
const _CenteredMessage({
|
||||
required this.title,
|
||||
this.message,
|
||||
this.actionLabel,
|
||||
this.onAction,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String? message;
|
||||
final String? actionLabel;
|
||||
final VoidCallback? onAction;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
if (message != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(message!, textAlign: TextAlign.center),
|
||||
],
|
||||
if (actionLabel != null && onAction != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
FilledButton(onPressed: onAction, child: Text(actionLabel!)),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user