import 'dart:math' as math; import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:trainhub_flutter/core/constants/ui_constants.dart'; import 'package:trainhub_flutter/core/theme/app_colors.dart'; import 'package:trainhub_flutter/domain/entities/workout_activity.dart'; import 'package:trainhub_flutter/presentation/workout_session/workout_session_controller.dart'; import 'package:trainhub_flutter/presentation/workout_session/workout_session_state.dart'; import 'package:trainhub_flutter/presentation/workout_session/widgets/activity_card.dart'; import 'package:trainhub_flutter/presentation/workout_session/widgets/session_controls.dart'; import 'package:trainhub_flutter/presentation/workout_session/widgets/session_progress_bar.dart'; @RoutePage() class WorkoutSessionPage extends ConsumerWidget { final String planId; const WorkoutSessionPage({ super.key, @PathParam('planId') required this.planId, }); @override Widget build(BuildContext context, WidgetRef ref) { final asyncState = ref.watch(workoutSessionControllerProvider(planId)); return Scaffold( backgroundColor: AppColors.zinc950, body: asyncState.when( loading: () => const Center( child: CircularProgressIndicator( color: AppColors.accent, strokeWidth: 2, ), ), error: (err, stack) => Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.error_outline_rounded, color: AppColors.destructive, size: 48, ), const SizedBox(height: UIConstants.spacing16), Text( 'Failed to load workout', style: TextStyle( color: AppColors.textPrimary, fontSize: 18, fontWeight: FontWeight.w600, ), ), const SizedBox(height: UIConstants.spacing8), Text( '$err', style: TextStyle(color: AppColors.textMuted, fontSize: 14), textAlign: TextAlign.center, ), const SizedBox(height: UIConstants.spacing16), OutlinedButton( onPressed: () => context.router.maybePop(), child: const Text('Go Back'), ), ], ), ), data: (state) { final controller = ref.read( workoutSessionControllerProvider(planId).notifier, ); if (state.isFinished) { return _CompletionScreen( totalTimeElapsed: state.totalTimeElapsed, ); } return _ActiveSessionView( state: state, controller: controller, ); }, ), ); } } class _ActiveSessionView extends StatefulWidget { final WorkoutSessionState state; final WorkoutSessionController controller; const _ActiveSessionView({ required this.state, required this.controller, }); @override State<_ActiveSessionView> createState() => _ActiveSessionViewState(); } class _ActiveSessionViewState extends State<_ActiveSessionView> { bool _showActivitiesList = false; @override Widget build(BuildContext context) { final activity = widget.state.currentActivity; final isRest = activity?.isRest ?? false; final isTimeBased = activity?.isTimeBased ?? false; final double timeProgress; if (activity != null && activity.duration > 0) { timeProgress = 1.0 - (widget.state.timeRemaining / activity.duration); } else { timeProgress = 0.0; } final accentTint = isRest ? AppColors.info.withValues(alpha: 0.06) : AppColors.accent.withValues(alpha: 0.06); final ringColor = isRest ? AppColors.info : AppColors.accent; return Stack( children: [ Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ AppColors.zinc950, accentTint, AppColors.zinc950, ], stops: const [0.0, 0.5, 1.0], ), ), child: Column( children: [ SessionProgressBar(progress: widget.state.progress), _buildTopBar(context), Expanded( child: Center( child: SingleChildScrollView( padding: const EdgeInsets.symmetric( horizontal: UIConstants.spacing24, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ if (activity != null) ActivityCard(activity: activity), const SizedBox(height: UIConstants.spacing32), if (isTimeBased) _CircularTimerDisplay( timeRemaining: widget.state.timeRemaining, progress: timeProgress, ringColor: ringColor, isRunning: widget.state.isRunning, ) else _RepsDisplay( reps: activity?.originalExercise?.value ?? 0, ), const SizedBox(height: UIConstants.spacing24), if (widget.state.nextActivity != null) _UpNextPill( nextActivityName: widget.state.nextActivity!.name, isNextRest: widget.state.nextActivity!.isRest, ), ], ), ), ), ), SessionControls( isRunning: widget.state.isRunning, isFinished: widget.state.isFinished, isTimeBased: isTimeBased, onPause: widget.controller.pauseTimer, onPlay: widget.controller.startTimer, onNext: widget.controller.next, onPrevious: widget.controller.previous, onRewind: () => widget.controller.rewindSeconds(10), onFastForward: () => widget.controller.rewindSeconds(-10), ), const SizedBox(height: UIConstants.spacing24), ], ), ), if (_showActivitiesList) _ActivitiesListPanel( activities: widget.state.activities, currentIndex: widget.state.currentIndex, onJumpTo: (index) { widget.controller.jumpTo(index); setState(() => _showActivitiesList = false); }, onClose: () => setState(() => _showActivitiesList = false), ), ], ); } Widget _buildTopBar(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric( horizontal: UIConstants.spacing16, vertical: UIConstants.spacing8, ), child: Row( children: [ IconButton( onPressed: () => _confirmExit(context), icon: const Icon( Icons.arrow_back_rounded, color: AppColors.textSecondary, ), tooltip: 'Exit workout', ), const Spacer(), _buildElapsedTimeBadge(), const SizedBox(width: UIConstants.spacing8), IconButton( onPressed: () => setState(() => _showActivitiesList = !_showActivitiesList), icon: Icon( _showActivitiesList ? Icons.list_rounded : Icons.format_list_bulleted_rounded, color: _showActivitiesList ? AppColors.accent : AppColors.textSecondary, ), tooltip: 'Exercise list', ), ], ), ); } Widget _buildElapsedTimeBadge() { return Container( padding: const EdgeInsets.symmetric( horizontal: UIConstants.spacing12, vertical: UIConstants.spacing4, ), decoration: BoxDecoration( color: AppColors.zinc800.withValues(alpha: 0.6), borderRadius: BorderRadius.circular(20), border: Border.all( color: AppColors.border.withValues(alpha: 0.5), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.timer_outlined, size: 14, color: AppColors.textMuted, ), const SizedBox(width: 4), Text( _formatDuration(widget.state.totalTimeElapsed), style: const TextStyle( color: AppColors.textSecondary, fontSize: 13, fontWeight: FontWeight.w500, fontFeatures: [FontFeature.tabularFigures()], ), ), ], ), ); } Future _confirmExit(BuildContext context) async { final shouldExit = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text('Exit Workout?'), content: const Text( 'Your progress will not be saved. Are you sure you want to exit?', ), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext, false), child: const Text('Cancel'), ), FilledButton( onPressed: () => Navigator.pop(dialogContext, true), style: FilledButton.styleFrom( backgroundColor: AppColors.destructive, ), child: const Text('Exit'), ), ], ), ); if (shouldExit == true && context.mounted) { widget.controller.pauseTimer(); context.router.maybePop(); } } String _formatDuration(int seconds) { final m = seconds ~/ 60; final s = seconds % 60; return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}'; } } class _RepsDisplay extends StatelessWidget { final int reps; const _RepsDisplay({required this.reps}); @override Widget build(BuildContext context) { const double size = 260; return SizedBox( width: size, height: size, child: Stack( alignment: Alignment.center, children: [ Container( width: size * 0.75, height: size * 0.75, decoration: BoxDecoration( shape: BoxShape.circle, boxShadow: [ BoxShadow( color: AppColors.accent.withValues(alpha: 0.15), blurRadius: 60, spreadRadius: 10, ), ], ), ), CustomPaint( size: const Size(size, size), painter: TimerRingPainter( progress: 1.0, ringColor: AppColors.accent, ), ), Column( mainAxisSize: MainAxisSize.min, children: [ Text( '$reps', style: TextStyle( color: AppColors.textPrimary, fontSize: 72, fontWeight: FontWeight.w200, letterSpacing: -2, shadows: [ Shadow( color: AppColors.accent.withValues(alpha: 0.3), blurRadius: 20, offset: const Offset(0, 4), ), ], ), ), const Text( 'REPS', style: TextStyle( color: AppColors.accent, fontSize: 16, fontWeight: FontWeight.w600, letterSpacing: 4, ), ), const SizedBox(height: 12), Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 6, ), decoration: BoxDecoration( color: AppColors.zinc800.withValues(alpha: 0.4), borderRadius: BorderRadius.circular(20), border: Border.all( color: AppColors.accent.withValues(alpha: 0.2), ), ), child: const Text( 'TAP NEXT WHEN DONE', style: TextStyle( color: AppColors.textSecondary, fontSize: 10, fontWeight: FontWeight.w500, letterSpacing: 1.5, ), ), ), ], ), ], ), ); } } class _CircularTimerDisplay extends StatelessWidget { final int timeRemaining; final double progress; final Color ringColor; final bool isRunning; const _CircularTimerDisplay({ required this.timeRemaining, required this.progress, required this.ringColor, required this.isRunning, }); @override Widget build(BuildContext context) { const double size = 260; return SizedBox( width: size, height: size, child: Stack( alignment: Alignment.center, children: [ Container( width: size * 0.75, height: size * 0.75, decoration: BoxDecoration( shape: BoxShape.circle, boxShadow: [ BoxShadow( color: ringColor.withValues(alpha: isRunning ? 0.15 : 0.05), blurRadius: 60, spreadRadius: 10, ), ], ), ), TweenAnimationBuilder( tween: Tween(begin: progress, end: progress), duration: const Duration(milliseconds: 400), curve: Curves.easeOutQuart, builder: (context, value, child) { return CustomPaint( size: const Size(size, size), painter: TimerRingPainter( progress: value, ringColor: ringColor, ), ); }, ), Column( mainAxisSize: MainAxisSize.min, children: [ Text( _formatTime(timeRemaining), style: TextStyle( color: AppColors.textPrimary, fontSize: 64, fontWeight: FontWeight.w200, letterSpacing: -1, fontFeatures: const [FontFeature.tabularFigures()], fontFamily: 'monospace', shadows: [ Shadow( color: ringColor.withValues(alpha: 0.3), blurRadius: 20, offset: const Offset(0, 4), ), ], ), ), AnimatedOpacity( opacity: isRunning ? 0.0 : 1.0, duration: const Duration(milliseconds: 200), child: Padding( padding: const EdgeInsets.only(top: 8), child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 4, ), decoration: BoxDecoration( color: AppColors.zinc800.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(12), border: Border.all( color: AppColors.zinc700.withValues(alpha: 0.5), ), ), child: Text( 'PAUSED', style: TextStyle( color: AppColors.textMuted, fontSize: 12, fontWeight: FontWeight.w600, letterSpacing: 2, ), ), ), ), ), ], ), ], ), ); } String _formatTime(int seconds) { final m = seconds ~/ 60; final s = seconds % 60; return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}'; } } class TimerRingPainter extends CustomPainter { final double progress; final Color ringColor; TimerRingPainter({ required this.progress, required this.ringColor, }); @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); final radius = math.min(size.width, size.height) / 2; const strokeWidth = 10.0; // Draw background track final trackPaint = Paint() ..color = AppColors.zinc800.withValues(alpha: 0.4) ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth; canvas.drawCircle(center, radius, trackPaint); if (progress <= 0.0) return; // Draw glowing shadow for the progress final shadowPaint = Paint() ..color = ringColor.withValues(alpha: 0.5) ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth * 2 ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 16); final sweepAngle = 2 * math.pi * progress; final startAngle = -math.pi / 2; canvas.drawArc( Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle, false, shadowPaint, ); // Draw progress ring with gradient final gradient = SweepGradient( startAngle: 0.0, endAngle: 2 * math.pi, colors: [ ringColor.withValues(alpha: 0.5), ringColor, ringColor.withValues(alpha: 0.8), ], transform: const GradientRotation(-math.pi / 2), ); final progressPaint = Paint() ..shader = gradient.createShader(Rect.fromCircle(center: center, radius: radius)) ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round ..strokeWidth = strokeWidth; canvas.drawArc( Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle, false, progressPaint, ); } @override bool shouldRepaint(TimerRingPainter oldDelegate) { return oldDelegate.progress != progress || oldDelegate.ringColor != ringColor; } } class _UpNextPill extends StatelessWidget { final String nextActivityName; final bool isNextRest; const _UpNextPill({ required this.nextActivityName, required this.isNextRest, }); @override Widget build(BuildContext context) { final pillColor = isNextRest ? AppColors.info.withValues(alpha: 0.12) : AppColors.accent.withValues(alpha: 0.12); final pillBorderColor = isNextRest ? AppColors.info.withValues(alpha: 0.25) : AppColors.accent.withValues(alpha: 0.25); final labelColor = isNextRest ? AppColors.info : AppColors.accent; return Container( padding: const EdgeInsets.symmetric( horizontal: UIConstants.spacing16, vertical: UIConstants.spacing8, ), decoration: BoxDecoration( color: pillColor, borderRadius: BorderRadius.circular(24), border: Border.all(color: pillBorderColor), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( 'UP NEXT', style: TextStyle( color: labelColor, fontSize: 10, fontWeight: FontWeight.w700, letterSpacing: 1.5, ), ), const SizedBox(width: UIConstants.spacing8), Container( width: 3, height: 3, decoration: BoxDecoration( color: AppColors.textMuted, shape: BoxShape.circle, ), ), const SizedBox(width: UIConstants.spacing8), Flexible( child: Text( nextActivityName, style: const TextStyle( color: AppColors.textSecondary, fontSize: 13, fontWeight: FontWeight.w500, ), overflow: TextOverflow.ellipsis, ), ), ], ), ); } } class _ActivitiesListPanel extends StatelessWidget { final List activities; final int currentIndex; final Function(int) onJumpTo; final VoidCallback onClose; const _ActivitiesListPanel({ required this.activities, required this.currentIndex, required this.onJumpTo, required this.onClose, }); @override Widget build(BuildContext context) { return Positioned.fill( child: GestureDetector( onTap: onClose, child: ColoredBox( color: Colors.black.withValues(alpha: 0.5), child: Align( alignment: Alignment.centerRight, child: GestureDetector( onTap: () {}, child: Container( width: 320, decoration: const BoxDecoration( color: AppColors.surfaceContainer, border: Border( left: BorderSide(color: AppColors.border), ), ), child: Column( children: [ Container( padding: const EdgeInsets.all(UIConstants.spacing16), decoration: const BoxDecoration( border: Border( bottom: BorderSide(color: AppColors.border), ), ), child: Row( children: [ const Text( 'All Exercises', style: TextStyle( color: AppColors.textPrimary, fontSize: 16, fontWeight: FontWeight.w600, ), ), const Spacer(), IconButton( onPressed: onClose, icon: const Icon( Icons.close, color: AppColors.textSecondary, ), ), ], ), ), Expanded( child: ListView.builder( itemCount: activities.length, itemBuilder: (context, index) { final activity = activities[index]; final isCurrent = index == currentIndex; final isRest = activity.isRest; return InkWell( onTap: () => onJumpTo(index), child: Container( padding: const EdgeInsets.symmetric( horizontal: UIConstants.spacing16, vertical: UIConstants.spacing12, ), decoration: BoxDecoration( color: isCurrent ? AppColors.accent.withValues(alpha: 0.12) : null, border: const Border( bottom: BorderSide( color: AppColors.border, width: 0.5, ), ), ), child: Row( children: [ Container( width: 8, height: 8, decoration: BoxDecoration( shape: BoxShape.circle, color: isCurrent ? AppColors.accent : isRest ? AppColors.info : AppColors.zinc600, ), ), const SizedBox(width: UIConstants.spacing12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( activity.name, style: TextStyle( color: isCurrent ? AppColors.textPrimary : AppColors.textSecondary, fontSize: 13, fontWeight: isCurrent ? FontWeight.w600 : FontWeight.normal, ), ), if (!isRest && activity.setIndex != null) Text( 'Set ${activity.setIndex}/${activity.totalSets} ยท ${activity.sectionName ?? ''}', style: const TextStyle( color: AppColors.textMuted, fontSize: 11, ), ), ], ), ), if (activity.isTimeBased) Text( _formatDuration(activity.duration), style: const TextStyle( color: AppColors.textMuted, fontSize: 12, fontFeatures: [ FontFeature.tabularFigures(), ], ), ) else if (!isRest) Text( '${activity.originalExercise?.value ?? 0} reps', style: const TextStyle( color: AppColors.textMuted, fontSize: 12, ), ), ], ), ), ); }, ), ), ], ), ), ), ), ), ), ); } String _formatDuration(int seconds) { final m = seconds ~/ 60; final s = seconds % 60; return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}'; } } class _CompletionScreen extends StatelessWidget { final int totalTimeElapsed; const _CompletionScreen({required this.totalTimeElapsed}); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ AppColors.zinc950, AppColors.success.withValues(alpha: 0.06), AppColors.zinc950, ], stops: const [0.0, 0.45, 1.0], ), ), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 96, height: 96, decoration: BoxDecoration( shape: BoxShape.circle, color: AppColors.success.withValues(alpha: 0.12), border: Border.all( color: AppColors.success.withValues(alpha: 0.3), width: 2, ), boxShadow: [ BoxShadow( color: AppColors.success.withValues(alpha: 0.15), blurRadius: 40, spreadRadius: 8, ), ], ), child: const Icon( Icons.check_rounded, color: AppColors.success, size: 48, ), ), const SizedBox(height: UIConstants.spacing24), const Text( 'Workout Complete', style: TextStyle( color: AppColors.textPrimary, fontSize: 28, fontWeight: FontWeight.w600, letterSpacing: -0.5, ), ), const SizedBox(height: UIConstants.spacing8), Text( 'Great job! You crushed it.', style: TextStyle( color: AppColors.textSecondary, fontSize: 15, ), ), const SizedBox(height: UIConstants.spacing32), Container( padding: const EdgeInsets.symmetric( horizontal: UIConstants.spacing24, vertical: UIConstants.spacing16, ), decoration: BoxDecoration( color: AppColors.surfaceContainer.withValues(alpha: 0.6), borderRadius: BorderRadius.circular(UIConstants.borderRadius), border: Border.all( color: AppColors.border.withValues(alpha: 0.5), ), ), child: Column( children: [ Text( 'TOTAL TIME', style: TextStyle( color: AppColors.textMuted, fontSize: 11, fontWeight: FontWeight.w600, letterSpacing: 1.5, ), ), const SizedBox(height: UIConstants.spacing4), Text( _formatDuration(totalTimeElapsed), style: TextStyle( color: AppColors.textPrimary, fontSize: 36, fontWeight: FontWeight.w300, fontFeatures: const [FontFeature.tabularFigures()], fontFamily: 'monospace', ), ), ], ), ), const SizedBox(height: UIConstants.spacing32), SizedBox( width: 200, height: 48, child: FilledButton( onPressed: () => context.router.maybePop(), style: FilledButton.styleFrom( backgroundColor: AppColors.success, foregroundColor: AppColors.zinc950, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(24), ), textStyle: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, ), ), child: const Text('Finish'), ), ), ], ), ), ); } String _formatDuration(int seconds) { final h = seconds ~/ 3600; final m = (seconds % 3600) ~/ 60; final s = seconds % 60; if (h > 0) { return '${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}'; } return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}'; } }