import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/utils/date_time_utils.dart'; import '../../domain/entities/exercise_entry.dart'; import '../controllers/exercise_controller.dart'; import '../widgets/log_exercise_dialog.dart'; /// Resistance-training log for lean mass preservation during GLP-1 therapy. /// /// The screen pairs a weekly strength-workout frequency card (sessions /// completed vs. the recommended three per week) with the full working-set /// log, grouped by day, plus an interactive "log exercise" dialog. class ExerciseScreen extends ConsumerWidget { const ExerciseScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(exerciseControllerProvider); ref.listen(exerciseControllerProvider, (previous, next) { if (next.errorMessage != null && next.errorMessage != previous?.errorMessage) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(next.errorMessage!), backgroundColor: Theme.of(context).colorScheme.error, ), ); ref.read(exerciseControllerProvider.notifier).clearError(); } }); return Scaffold( appBar: AppBar(title: const Text('Exercise Log')), body: ListView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 88), children: [ _WeeklyFrequencyCard( sessionsThisWeek: state.weeklyWorkoutCount, targetSessions: ExerciseController.recommendedWeeklySessions, ), const SizedBox(height: 16), Text( 'Working Sets', style: Theme.of(context) .textTheme .titleMedium ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), if (state.entries.isEmpty) const _EmptyLogHint() else ..._buildDayGroups(context, state), ], ), floatingActionButton: FloatingActionButton.extended( onPressed: () => _showLogDialog(context), icon: const Icon(Icons.add), label: const Text('Log Exercise'), ), ); } /// Groups the log by calendar day (newest day first, newest set first). List _buildDayGroups(BuildContext context, ExerciseState state) { final byDay = >{}; for (final entry in state.entries) { byDay.putIfAbsent(startOfDay(entry.timestamp), () => []).add(entry); } final days = byDay.keys.toList()..sort((a, b) => b.compareTo(a)); return [ for (final day in days) ...[ Padding( padding: const EdgeInsets.only(top: 8, bottom: 4), child: Text( formatMonthDay(day), style: Theme.of(context) .textTheme .labelLarge ?.copyWith(color: Theme.of(context).colorScheme.primary), ), ), for (final entry in byDay[day]!) _ExerciseTile(entry: entry), ], ]; } Future _showLogDialog(BuildContext context) { return showDialog( context: context, builder: (context) => const LogExerciseDialog(), ); } } /// Card surfacing weekly strength-workout frequency against the lean-mass /// preservation recommendation (three resistance sessions per week). class _WeeklyFrequencyCard extends StatelessWidget { const _WeeklyFrequencyCard({ required this.sessionsThisWeek, required this.targetSessions, }); final int sessionsThisWeek; final int targetSessions; @override Widget build(BuildContext context) { final theme = Theme.of(context); final progress = (sessionsThisWeek / targetSessions).clamp(0.0, 1.0).toDouble(); final goalMet = sessionsThisWeek >= targetSessions; return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.fitness_center, color: goalMet ? Colors.green.shade700 : theme.colorScheme.primary, ), const SizedBox(width: 8), Expanded( child: Text( 'Weekly Strength Frequency', style: theme.textTheme.titleSmall ?.copyWith(fontWeight: FontWeight.bold), ), ), if (goalMet) Chip( visualDensity: VisualDensity.compact, backgroundColor: Colors.green.shade100, label: Text( 'Goal met', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.green.shade800, ), ), ), ], ), const SizedBox(height: 12), Text( '$sessionsThisWeek of $targetSessions sessions this week', style: theme.textTheme.titleMedium ?.copyWith(fontWeight: FontWeight.w600), ), const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: progress, minHeight: 8, backgroundColor: theme.colorScheme.surfaceContainerHighest, ), ), const SizedBox(height: 8), Text( 'Resistance training preserves lean mass and offsets ' 'GLP-1-driven muscle loss — aim for $targetSessions full-body ' 'or split sessions per week.', style: theme.textTheme.bodySmall ?.copyWith(color: theme.colorScheme.outline), ), ], ), ), ); } } /// One logged working-set row with a swipe-to-delete affordance. class _ExerciseTile extends ConsumerWidget { const _ExerciseTile({required this.entry}); final ExerciseEntry entry; @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); return Dismissible( key: ValueKey(entry.id), direction: DismissDirection.endToStart, background: Container( alignment: Alignment.centerRight, padding: const EdgeInsets.only(right: 16), color: theme.colorScheme.errorContainer, child: Icon(Icons.delete_outline, color: theme.colorScheme.error), ), onDismissed: (_) => ref .read(exerciseControllerProvider.notifier) .deleteEntry(entry.id), child: Card( margin: const EdgeInsets.symmetric(vertical: 4), child: ListTile( leading: CircleAvatar( backgroundColor: theme.colorScheme.secondaryContainer, child: Icon( Icons.fitness_center, size: 20, color: theme.colorScheme.onSecondaryContainer, ), ), title: Text( entry.exerciseName, style: const TextStyle(fontWeight: FontWeight.w600), ), subtitle: Text( '${entry.muscleGroup.displayName} · ${entry.volumeLabel}', ), trailing: Text( formatDateTime(entry.timestamp), style: theme.textTheme.labelSmall ?.copyWith(color: theme.colorScheme.outline), ), ), ), ); } } /// Placeholder shown while the log has no entries. class _EmptyLogHint extends StatelessWidget { const _EmptyLogHint(); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Padding( padding: const EdgeInsets.symmetric(vertical: 32), child: Column( children: [ Icon(Icons.fitness_center_outlined, size: 40, color: theme.colorScheme.outline), const SizedBox(height: 8), Text( 'No sessions logged yet.\nTap "Log Exercise" to record your ' 'first working set.', textAlign: TextAlign.center, style: theme.textTheme.bodySmall ?.copyWith(fontStyle: FontStyle.italic), ), ], ), ); } }