glp-companion/lib/features/meal_planner/presentation/screens/meal_planner_screen.dart

187 lines
6.4 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../nutrition/domain/entities/meal_type.dart';
import '../controllers/meal_plan_calendar_controller.dart';
import '../controllers/recipe_import_controller.dart';
import '../widgets/assign_recipe_sheet.dart';
import '../widgets/weekly_calendar_view.dart';
/// GLP-1 meal planner: the responsive 7-day weekly calendar (Monday–Sunday)
/// with per-day protein rollups. Recipe import and browsing live in the
/// Recipe Vault; slots are filled via the searchable "+ Assign Recipe"
/// sheet.
class MealPlannerScreen extends ConsumerStatefulWidget {
const MealPlannerScreen({super.key});
@override
ConsumerState<MealPlannerScreen> createState() => _MealPlannerScreenState();
}
class _MealPlannerScreenState extends ConsumerState<MealPlannerScreen> {
/// Body height below which the page collapses into a single scrollable
/// column: the week header plus a minimal usable calendar area. Below
/// this, a plain `Column` would overflow and paint the yellow/black
/// hatched stripes.
static const double _kFullScrollThreshold = 360;
/// Calendar height used in the fully-scrollable fallback layout; tall
/// enough for the day header plus four meal slots.
static const double _kCalendarScrollHeight = 560;
@override
Widget build(BuildContext context) {
final calendarState = ref.watch(mealPlanCalendarControllerProvider);
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')),
body: LayoutBuilder(
builder: (context, constraints) {
// Short windows (e.g. resized desktop windows) can no longer fit
// 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: [
_WeekNavigationHeader(state: calendarState),
SizedBox(
height: _kCalendarScrollHeight,
child: _buildCalendar(calendarState),
),
],
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_WeekNavigationHeader(state: calendarState),
const Divider(height: 1),
Expanded(child: _buildCalendar(calendarState)),
],
);
},
),
);
}
/// Weekly calendar grid (or the initial loading spinner).
Widget _buildCalendar(MealPlanCalendarState calendarState) =>
!calendarState.hasLoaded
? const Center(child: CircularProgressIndicator())
: WeeklyCalendarView(
state: calendarState,
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,
),
);
}
/// Opens the searchable assign sheet, populated from the Recipe Vault.
Future<void> _onSlotTapped(DateTime day, MealType mealType) async {
final calendarState = ref.read(mealPlanCalendarControllerProvider);
final importState = ref.read(recipeImportControllerProvider);
await showAssignRecipeSheet(
context,
day: day,
mealType: mealType,
savedRecipes: importState.savedRecipes,
existingSlot: calendarState.slotFor(day, mealType),
);
}
}
/// 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);
}