import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:trainhub_flutter/core/theme/app_colors.dart'; import 'package:trainhub_flutter/core/constants/ui_constants.dart'; import 'package:trainhub_flutter/core/router/app_router.dart'; import 'package:trainhub_flutter/domain/entities/training_plan.dart'; import 'package:trainhub_flutter/domain/entities/exercise.dart'; import 'package:trainhub_flutter/presentation/trainings/trainings_controller.dart'; import 'package:trainhub_flutter/presentation/common/widgets/app_empty_state.dart'; import 'package:trainhub_flutter/presentation/common/dialogs/text_input_dialog.dart'; import 'package:trainhub_flutter/presentation/common/dialogs/confirm_dialog.dart'; @RoutePage() class TrainingsPage extends ConsumerWidget { const TrainingsPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final asyncState = ref.watch(trainingsControllerProvider); return DefaultTabController( length: 2, child: Column( children: [ Container( decoration: BoxDecoration( color: AppColors.surfaceContainer, border: const Border(bottom: BorderSide(color: AppColors.border)), ), child: const TabBar( tabs: [ Tab(text: 'Training Plans'), Tab(text: 'Exercises'), ], ), ), Expanded( child: asyncState.when( loading: () => const Center(child: CircularProgressIndicator()), error: (err, _) => Center(child: Text('Error: $err')), data: (state) => TabBarView( children: [ _PlansTab(plans: state.plans, ref: ref), _ExercisesTab(exercises: state.exercises, ref: ref), ], ), ), ), ], ), ); } } class _PlansTab extends StatelessWidget { final List plans; final WidgetRef ref; const _PlansTab({required this.plans, required this.ref}); @override Widget build(BuildContext context) { if (plans.isEmpty) { return AppEmptyState( icon: Icons.fitness_center, title: 'No training plans yet', subtitle: 'Create your first training plan to get started', actionLabel: 'Create Plan', onAction: () => _createPlan(context), ); } return Column( children: [ Padding( padding: const EdgeInsets.all(UIConstants.spacing16), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ FilledButton.icon( onPressed: () => _createPlan(context), icon: const Icon(Icons.add, size: 18), label: const Text('New Plan'), ), ], ), ), Expanded( child: ListView.builder( padding: const EdgeInsets.symmetric( horizontal: UIConstants.spacing16, ), itemCount: plans.length, itemBuilder: (context, index) { final plan = plans[index]; return _PlanListItem( plan: plan, onEdit: () { context.router.push(PlanEditorRoute(planId: plan.id)); }, onStart: () { context.router.push(WorkoutSessionRoute(planId: plan.id)); }, onDelete: () => _deletePlan(context, plan), ); }, ), ), ], ); } Future _createPlan(BuildContext context) async { final name = await TextInputDialog.show( context, title: 'New Plan Name', hintText: 'e.g. Push Pull Legs', ); if (name != null && name.isNotEmpty) { final plan = await ref .read(trainingsControllerProvider.notifier) .createPlan(name); if (context.mounted) { context.router.push(PlanEditorRoute(planId: plan.id)); } } } Future _deletePlan( BuildContext context, TrainingPlanEntity plan, ) async { final confirmed = await ConfirmDialog.show( context, title: 'Delete Plan?', message: 'Are you sure you want to delete "${plan.name}"?', ); if (confirmed == true) { ref.read(trainingsControllerProvider.notifier).deletePlan(plan.id); } } } class _PlanListItem extends StatefulWidget { final TrainingPlanEntity plan; final VoidCallback onEdit; final VoidCallback onStart; final VoidCallback onDelete; const _PlanListItem({ required this.plan, required this.onEdit, required this.onStart, required this.onDelete, }); @override State<_PlanListItem> createState() => _PlanListItemState(); } class _PlanListItemState extends State<_PlanListItem> { bool _isHovered = false; @override Widget build(BuildContext context) { return MouseRegion( onEnter: (_) => setState(() => _isHovered = true), onExit: (_) => setState(() => _isHovered = false), child: Card( margin: const EdgeInsets.only(bottom: UIConstants.spacing8), child: Padding( padding: const EdgeInsets.all(UIConstants.spacing12), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: AppColors.accentMuted, borderRadius: BorderRadius.circular(8), ), child: const Icon( Icons.description_outlined, color: AppColors.accent, size: 20, ), ), const SizedBox(width: UIConstants.spacing12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.plan.name, style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 14, ), ), const SizedBox(height: 2), Text( '${widget.plan.sections.length} sections, ${widget.plan.totalExercises} exercises', style: const TextStyle( color: AppColors.textMuted, fontSize: 12, ), ), ], ), ), if (_isHovered) ...[ IconButton( icon: const Icon( Icons.delete_outline, color: AppColors.destructive, size: 18, ), onPressed: widget.onDelete, tooltip: 'Delete', ), const SizedBox(width: 4), ], OutlinedButton( onPressed: widget.onEdit, child: const Text('Edit'), ), const SizedBox(width: UIConstants.spacing8), FilledButton.icon( onPressed: widget.onStart, icon: const Icon(Icons.play_arrow, size: 18), label: const Text('Start'), ), ], ), ), ), ); } } class _ExercisesTab extends StatelessWidget { final List exercises; final WidgetRef ref; const _ExercisesTab({required this.exercises, required this.ref}); @override Widget build(BuildContext context) { return Scaffold( floatingActionButton: FloatingActionButton( onPressed: () => _showExerciseDialog(context), backgroundColor: AppColors.zinc50, foregroundColor: AppColors.zinc950, child: const Icon(Icons.add), ), body: exercises.isEmpty ? const AppEmptyState( icon: Icons.fitness_center, title: 'No exercises yet', subtitle: 'Add exercises to use in your training plans', ) : GridView.builder( padding: const EdgeInsets.all(UIConstants.spacing16), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, childAspectRatio: 3.0, crossAxisSpacing: UIConstants.spacing12, mainAxisSpacing: UIConstants.spacing12, ), itemCount: exercises.length, itemBuilder: (context, index) { final exercise = exercises[index]; return _ExerciseCard( exercise: exercise, onEdit: () => _showExerciseDialog(context, exercise: exercise), onDelete: () => ref .read(trainingsControllerProvider.notifier) .deleteExercise(exercise.id), ); }, ), ); } void _showExerciseDialog(BuildContext context, {ExerciseEntity? exercise}) { final nameCtrl = TextEditingController(text: exercise?.name); final instructionsCtrl = TextEditingController( text: exercise?.instructions, ); final tagsCtrl = TextEditingController(text: exercise?.tags); final videoUrlCtrl = TextEditingController(text: exercise?.videoUrl); showDialog( context: context, builder: (context) => AlertDialog( title: Text(exercise == null ? 'New Exercise' : 'Edit Exercise'), content: SizedBox( width: UIConstants.dialogWidth, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: nameCtrl, decoration: const InputDecoration(labelText: 'Name'), autofocus: true, ), const SizedBox(height: UIConstants.spacing12), TextField( controller: instructionsCtrl, decoration: const InputDecoration(labelText: 'Instructions'), maxLines: 2, ), const SizedBox(height: UIConstants.spacing12), TextField( controller: tagsCtrl, decoration: const InputDecoration( labelText: 'Tags (comma separated)', ), ), const SizedBox(height: UIConstants.spacing12), TextField( controller: videoUrlCtrl, decoration: const InputDecoration(labelText: 'Video URL'), ), ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Cancel'), ), FilledButton( onPressed: () { if (nameCtrl.text.isEmpty) return; if (exercise == null) { ref .read(trainingsControllerProvider.notifier) .addExercise( name: nameCtrl.text, instructions: instructionsCtrl.text, tags: tagsCtrl.text, videoUrl: videoUrlCtrl.text, ); } else { ref .read(trainingsControllerProvider.notifier) .updateExercise( exercise.copyWith( name: nameCtrl.text, instructions: instructionsCtrl.text, tags: tagsCtrl.text, videoUrl: videoUrlCtrl.text, ), ); } Navigator.pop(context); }, child: const Text('Save'), ), ], ), ); } } class _ExerciseCard extends StatefulWidget { final ExerciseEntity exercise; final VoidCallback onEdit; final VoidCallback onDelete; const _ExerciseCard({ required this.exercise, required this.onEdit, required this.onDelete, }); @override State<_ExerciseCard> createState() => _ExerciseCardState(); } class _ExerciseCardState extends State<_ExerciseCard> { bool _isHovered = false; @override Widget build(BuildContext context) { return MouseRegion( onEnter: (_) => setState(() => _isHovered = true), onExit: (_) => setState(() => _isHovered = false), child: Card( child: InkWell( onTap: widget.onEdit, borderRadius: UIConstants.cardBorderRadius, child: Padding( padding: const EdgeInsets.all(UIConstants.spacing12), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Text( widget.exercise.name, style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 14, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), if (widget.exercise.instructions != null && widget.exercise.instructions!.isNotEmpty) ...[ const SizedBox(height: 4), Text( widget.exercise.instructions!, style: const TextStyle( color: AppColors.textMuted, fontSize: 12, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], if (widget.exercise.tags != null && widget.exercise.tags!.isNotEmpty) ...[ const SizedBox(height: 6), Wrap( spacing: 4, children: widget.exercise.tags! .split(',') .take(3) .map( (tag) => Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: AppColors.zinc800, borderRadius: BorderRadius.circular(4), ), child: Text( tag.trim(), style: const TextStyle( fontSize: 10, color: AppColors.textSecondary, ), ), ), ) .toList(), ), ], ], ), ), if (widget.exercise.videoUrl != null && widget.exercise.videoUrl!.isNotEmpty) const Padding( padding: EdgeInsets.only(right: 8), child: Icon( Icons.videocam, size: 16, color: AppColors.info, ), ), if (_isHovered) ...[ IconButton( icon: const Icon(Icons.edit, size: 16), onPressed: widget.onEdit, ), IconButton( icon: const Icon( Icons.delete_outline, size: 16, color: AppColors.destructive, ), onPressed: widget.onDelete, ), ], ], ), ), ), ), ); } }