import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../nutrition/domain/entities/meal_type.dart'; import '../../domain/entities/recipe.dart'; import '../controllers/meal_plan_calendar_controller.dart'; import '../controllers/recipe_import_controller.dart'; import '../widgets/assign_slot_dialog.dart'; import '../widgets/recipe_preview_dialog.dart'; import '../widgets/saved_recipes_tray.dart'; import '../widgets/weekly_calendar_view.dart'; /// GLP-1 meal planner: recipe URL import bar, saved-recipes tray, and the /// responsive weekly calendar with drag-and-drop assignment and per-day /// protein rollups. class MealPlannerScreen extends ConsumerStatefulWidget { const MealPlannerScreen({super.key}); @override ConsumerState createState() => _MealPlannerScreenState(); } class _MealPlannerScreenState extends ConsumerState { /// Body height below which the page collapses into a single scrollable /// column: fixed top sections (~340 dp) plus a minimal usable calendar /// area. Below this, a plain `Column` would overflow and paint the /// yellow/black hatched stripes. static const double _kFullScrollThreshold = 520; /// Calendar height used in the fully-scrollable fallback layout; tall /// enough for the day header plus four meal slots. static const double _kCalendarScrollHeight = 560; final TextEditingController _urlController = TextEditingController(); bool _previewDialogVisible = false; @override void dispose() { _urlController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final importState = ref.watch(recipeImportControllerProvider); final calendarState = ref.watch(mealPlanCalendarControllerProvider); // Open the preview modal when a scrape completes; surface messages as // snackbars. ref.listen(recipeImportControllerProvider, (previous, next) { if (next.previewRecipe != null && previous?.previewRecipe == null && !_previewDialogVisible) { _showPreview(next.previewRecipe!); } if (next.statusMessage != null && next.statusMessage != previous?.statusMessage) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(next.statusMessage!))); } if (next.errorMessage != null && next.errorMessage != previous?.errorMessage && next.previewRecipe == null) { _showErrorSnack(next.errorMessage!); } }); ref.listen(mealPlanCalendarControllerProvider, (previous, next) { if (next.errorMessage != null && next.errorMessage != previous?.errorMessage) { _showErrorSnack(next.errorMessage!); } }); return Scaffold( appBar: AppBar(title: const Text('Meal Planner & Import')), body: LayoutBuilder( builder: (context, constraints) { // Short windows (e.g. resized desktop windows) can no longer fit // the fixed top sections plus a useful calendar area; collapse the // whole page into a single scroll view instead of overflowing. if (constraints.maxHeight < _kFullScrollThreshold) { return SingleChildScrollView( padding: const EdgeInsets.only(bottom: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildTopSections(importState, calendarState), SizedBox( height: _kCalendarScrollHeight, child: _buildCalendar(calendarState), ), ], ), ); } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildTopSections(importState, calendarState), Expanded(child: _buildCalendar(calendarState)), ], ); }, ), ); } /// Import bar + saved recipes tray + week navigation header. Widget _buildTopSections( RecipeImportState importState, MealPlanCalendarState calendarState, ) => Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _RecipeImportBar( controller: _urlController, isLoading: importState.isImporting, onSubmit: _submitUrl, ), const SizedBox(height: 12), SavedRecipesTray( recipes: importState.savedRecipes, onAssignPressed: _openTrayAssign, ), const SizedBox(height: 12), _WeekNavigationHeader(state: calendarState), const Divider(height: 1), ], ); /// Weekly calendar grid (or the initial loading spinner). Widget _buildCalendar(MealPlanCalendarState calendarState) => !calendarState.hasLoaded ? const Center(child: CircularProgressIndicator()) : WeeklyCalendarView( state: calendarState, onRecipeDropped: _onRecipeDropped, onSlotTapped: _onSlotTapped, onTogglePrepared: (slot) => ref .read(mealPlanCalendarControllerProvider.notifier) .togglePrepared(slot), onClearSlot: (slot) => ref .read(mealPlanCalendarControllerProvider.notifier) .clearSlot(slot), ); void _showErrorSnack(String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: Theme.of(context).colorScheme.error, ), ); } Future _submitUrl() async { final url = _urlController.text; final notifier = ref.read(recipeImportControllerProvider.notifier); await notifier.submitUrl(url); if (mounted) _urlController.clear(); } Future _showPreview(Recipe recipe) async { _previewDialogVisible = true; final notifier = ref.read(recipeImportControllerProvider.notifier); final saved = await showRecipePreviewDialog(context, recipe: recipe); _previewDialogVisible = false; // Cancelling (or abandoning a failed save) clears the parked preview so // the modal never re-opens on the next rebuild. if (saved != true) notifier.dismissPreview(); } void _onRecipeDropped(Recipe recipe, DateTime day, MealType mealType) { ref.read(mealPlanCalendarControllerProvider.notifier).assignRecipe( day: day, mealType: mealType, recipeId: recipe.id, ); } void _onSlotTapped(DateTime day, MealType mealType) { final calendarState = ref.read(mealPlanCalendarControllerProvider); final importState = ref.read(recipeImportControllerProvider); showAssignSlotDialog( context, day: day, mealType: mealType, savedRecipes: importState.savedRecipes, existingSlot: calendarState.slotFor(day, mealType), ); } void _openTrayAssign(Recipe recipe) { final calendarState = ref.read(mealPlanCalendarControllerProvider); final importState = ref.read(recipeImportControllerProvider); // Default target: today when inside the window, else the first day. final now = DateTime.now(); final day = calendarState.days.firstWhere( (day) => day.year == now.year && day.month == now.month && day.day == now.day, orElse: () => calendarState.days.first, ); showAssignSlotDialog( context, day: day, mealType: MealType.breakfast, savedRecipes: importState.savedRecipes, existingSlot: calendarState.slotFor(day, MealType.breakfast), ); } } /// URL import bar: text field + Import button with an inline loading /// spinner. class _RecipeImportBar extends StatelessWidget { const _RecipeImportBar({ required this.controller, required this.isLoading, required this.onSubmit, }); final TextEditingController controller; final bool isLoading; final VoidCallback onSubmit; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), child: Row( children: [ Expanded( child: TextField( controller: controller, enabled: !isLoading, keyboardType: TextInputType.url, decoration: const InputDecoration( labelText: 'Recipe URL', hintText: 'https://example.com/best-lasagna', border: OutlineInputBorder(), isDense: true, prefixIcon: Icon(Icons.link), ), onSubmitted: (_) => onSubmit(), ), ), const SizedBox(width: 8), FilledButton.icon( onPressed: isLoading ? null : onSubmit, icon: isLoading ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.download_outlined), label: const Text('Import'), ), ], ), ); } } /// Week navigation row: prev/next arrows, the visible week label, and a /// "Today" shortcut. class _WeekNavigationHeader extends StatelessWidget { const _WeekNavigationHeader({required this.state}); final MealPlanCalendarState state; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Row( children: [ IconButton( tooltip: 'Previous week', icon: const Icon(Icons.chevron_left), onPressed: state.isLoading ? null : () => _readController(context).previousWeek(), ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( state.weekLabel, style: theme.textTheme.titleSmall ?.copyWith(fontWeight: FontWeight.bold), ), Text( 'Monday – Sunday', style: theme.textTheme.labelSmall ?.copyWith(color: theme.colorScheme.outline), ), ], ), ), TextButton( onPressed: state.isLoading ? null : () => _readController(context).goToToday(), child: const Text('Today'), ), IconButton( tooltip: 'Next week', icon: const Icon(Icons.chevron_right), onPressed: state.isLoading ? null : () => _readController(context).nextWeek(), ), if (state.isLoading) ...[ const SizedBox(width: 4), const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ), const SizedBox(width: 8), ], ], ), ); } MealPlanCalendarController _readController(BuildContext context) => ProviderScope.containerOf(context, listen: false) .read(mealPlanCalendarControllerProvider.notifier); }