import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../design/number_stepper.dart'; import '../../design/page_header.dart'; import '../../design/zen_style.dart'; import '../../exercises/exercise_library.dart'; import '../../exercises/views/library_view.dart'; import '../../localization/app_localizations.dart'; import '../../modes/mode.dart'; import '../models/plan.dart'; import '../plan_labels.dart'; import '../plan_repository.dart'; import 'exercise_picker_view.dart'; import 'plans_view.dart'; import 'step_editor_sheet.dart'; /// Builds one Plan: its Steps and Supersets in order, each editable, movable /// by its handle and removable. Every change is saved at once. class PlanEditorView extends StatelessWidget { const PlanEditorView({super.key, required this.planId}); static const routeName = '/plan'; final String planId; @override Widget build(BuildContext context) { final plans = context.read(); return StreamBuilder( stream: plans.watchPlan(planId), builder: (context, snapshot) { final plan = snapshot.data; if (plan == null) return const Scaffold(); return _Editor(plan: plan, save: plans.save); }, ); } } class _Editor extends StatelessWidget { const _Editor({required this.plan, required this.save}); final Plan plan; final Future Function(Plan) save; void _addExercises(BuildContext context) => Navigator.of(context).pushNamed(ExercisePickerView.routeName, arguments: plan.id); @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final bottomInset = MediaQuery.paddingOf(context).bottom; final addButton = FilledButton.icon( onPressed: () => _addExercises(context), icon: const Icon(Icons.add_rounded), label: Text(l10n.addExercises), ); return Scaffold( appBar: AppBar( actions: [PlanMenu(plan: plan, onDeleted: () => Navigator.of(context).pop())], ), body: plan.items.isEmpty ? ListView( children: [ PageHeader(title: plan.name), const SizedBox(height: 16), EmptyState( icon: Icons.add_task_rounded, title: l10n.planEmptyTitle, body: l10n.planEmptyBody(plan.mode.name), ), Padding( padding: const EdgeInsets.fromLTRB(20, 20, 20, 0), child: Row(children: [addButton]), ), ], ) : ReorderableListView.builder( buildDefaultDragHandles: false, padding: EdgeInsets.only(bottom: 96 + bottomInset), header: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ PageHeader(title: plan.name, subtitle: planSummary(l10n, plan)), if (plan.mode == Mode.stretching) Padding( padding: const EdgeInsets.fromLTRB(20, 0, 16, 12), child: NumberStepper( label: l10n.transitionLabel, value: l10n.secondsShort(plan.transitionSeconds), onDecrement: plan.transitionSeconds > 0 ? () => save(plan.copyWith(transitionSeconds: plan.transitionSeconds - 1)) : null, onIncrement: plan.transitionSeconds < 30 ? () => save(plan.copyWith(transitionSeconds: plan.transitionSeconds + 1)) : null, ), ), ], ), itemCount: plan.items.length, onReorder: (from, to) => save(plan.withItemMoved(from, to)), proxyDecorator: (child, _, _) => Material(color: Colors.transparent, elevation: 6, child: child), itemBuilder: (context, i) => Padding( key: ObjectKey(plan.items[i]), padding: const EdgeInsets.fromLTRB(16, 0, 16, 10), child: switch (plan.items[i]) { final Superset superset => _SupersetCard(plan: plan, index: i, superset: superset, save: save), final item => _StepTile(plan: plan, index: i, item: item, save: save), }, ), ), floatingActionButton: plan.items.isEmpty ? null : FloatingActionButton.extended( onPressed: () => _addExercises(context), icon: const Icon(Icons.add_rounded), label: Text(l10n.addExercises), ), ); } } /// A Workout or Stretching Step on its own. class _StepTile extends StatelessWidget { const _StepTile({required this.plan, required this.index, required this.item, required this.save}); final Plan plan; final int index; final PlanItem item; final Future Function(Plan) save; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final (exerciseId, summary) = switch (item) { final WorkoutStep step => (step.exerciseId, workoutStepSummary(l10n, step)), final StretchingStep step => (step.exerciseId, stretchingStepSummary(l10n, step)), Superset() => throw StateError('Supersets have their own card'), }; final name = _exerciseName(context, exerciseId); return Card( child: _ExerciseRow( exerciseId: exerciseId, name: name, summary: summary, dragIndex: index, onTap: () => showStepEditor( context, title: name, value: item, onChanged: (edited) => save(plan.withItemReplaced(index, edited)), ), menu: [ if (plan.canSupersetWithNext(index)) _MenuAction(l10n.supersetWithNext, () => save(plan.withSupersetWithNext(index))), _MenuAction(l10n.remove, () => save(plan.withItemRemoved(index))), ], ), ); } } /// A Superset: a header with its set count and rest choice, its members /// below, all held together by an accent bar on the left. class _SupersetCard extends StatelessWidget { const _SupersetCard({required this.plan, required this.index, required this.superset, required this.save}); final Plan plan; final int index; final Superset superset; final Future Function(Plan) save; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final s = ZenStyle.of(context); final text = Theme.of(context).textTheme; return Card( child: IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container(width: 4, decoration: BoxDecoration(gradient: s.accentGradient)), Expanded( child: Column( children: [ InkWell( onTap: () => showStepEditor( context, title: l10n.supersetTitle, value: superset, onChanged: (edited) => save(plan.withItemReplaced(index, edited)), ), child: Padding( padding: const EdgeInsets.fromLTRB(14, 10, 0, 4), child: Row( children: [ Icon(Icons.link_rounded, size: 20, color: s.accent), const SizedBox(width: 8), Expanded( child: Text( '${l10n.supersetTitle} ยท ${l10n.setsCount(superset.sets)}', style: text.titleSmall?.copyWith(color: s.accent), ), ), _Menu([ if (plan.canSupersetWithNext(index)) _MenuAction(l10n.supersetWithNext, () => save(plan.withSupersetWithNext(index))), _MenuAction(l10n.ungroup, () => save(plan.withUngrouped(index))), _MenuAction(l10n.remove, () => save(plan.withItemRemoved(index))), ]), _DragHandle(index: index), ], ), ), ), for (final (m, member) in superset.members.indexed) _ExerciseRow( exerciseId: member.exerciseId, name: _exerciseName(context, member.exerciseId), summary: memberSummary(l10n, member), onTap: () => showStepEditor( context, title: _exerciseName(context, member.exerciseId), value: member, onChanged: (edited) => save( plan.withItemReplaced(index, superset.copyWith(members: [...superset.members]..[m] = edited)), ), ), menu: [ _MenuAction(l10n.takeOutOfSuperset, () => save(plan.withMemberRemovedFromSuperset(index, m))), ], ), ], ), ), ], ), ), ); } } String _exerciseName(BuildContext context, String exerciseId) => context.read().byId(exerciseId)?.name ?? exerciseId; class _ExerciseRow extends StatelessWidget { const _ExerciseRow({ required this.exerciseId, required this.name, required this.summary, required this.onTap, required this.menu, this.dragIndex, }); final String exerciseId; final String name; final String summary; final VoidCallback onTap; final List<_MenuAction> menu; /// Set for items of the Plan's own list, which can be dragged. final int? dragIndex; @override Widget build(BuildContext context) { final s = ZenStyle.of(context); final text = Theme.of(context).textTheme; final exercise = context.read().byId(exerciseId); return InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.fromLTRB(10, 8, 0, 8), child: Row( children: [ if (exercise != null) ExerciseThumbnail(exercise: exercise, size: 52), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(name, style: text.titleSmall, maxLines: 2, overflow: TextOverflow.ellipsis), const SizedBox(height: 2), Text(summary, style: text.bodySmall?.copyWith(color: s.inkMuted)), ], ), ), _Menu(menu), if (dragIndex != null) _DragHandle(index: dragIndex!) else const SizedBox(width: 8), ], ), ), ); } } class _DragHandle extends StatelessWidget { const _DragHandle({required this.index}); final int index; @override Widget build(BuildContext context) { final s = ZenStyle.of(context); return ReorderableDragStartListener( index: index, child: Tooltip( message: AppLocalizations.of(context).dragToReorder, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), child: Icon(Icons.drag_indicator_rounded, color: s.inkMuted), ), ), ); } } class _MenuAction { const _MenuAction(this.label, this.onSelected); final String label; final VoidCallback onSelected; } class _Menu extends StatelessWidget { const _Menu(this.actions); final List<_MenuAction> actions; @override Widget build(BuildContext context) => PopupMenuButton( tooltip: AppLocalizations.of(context).moreActions, icon: Icon(Icons.more_vert_rounded, color: ZenStyle.of(context).inkMuted), onSelected: (i) => actions[i].onSelected(), itemBuilder: (context) => [for (final (i, a) in actions.indexed) PopupMenuItem(value: i, child: Text(a.label))], ); }