Initial commit: CookIT recipe collection and week planner

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
CedricandClaude Opus 5.5 committed 2026-10-28 22:55:09 +01:00
commit 7d7a0bc60b
173 files changed
+22803

No files matched your search

@@ -0,0 +1,154 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../design/cookit_colors.dart';
import '../../../localization/app_localizations.dart';
import '../../../recipes/recipe_list_provider.dart';
import '../../../recipes/views/widgets/recipe_photo.dart';
import '../../day.dart';
import '../../day_format.dart';
import '../../models/meal.dart';
import '../../plan_repository.dart';
/// "+" on a Meal: search the collection for a Recipe, or keep what was typed as a note.
Future<void> showAddEntrySheet(BuildContext context, DateTime day, Meal meal) => showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
showDragHandle: true,
builder: (_) => _AddEntrySheet(day: day, meal: meal),
);
class _AddEntrySheet extends StatefulWidget {
const _AddEntrySheet({required this.day, required this.meal});
final DateTime day;
final Meal meal;
@override
State<_AddEntrySheet> createState() => _AddEntrySheetState();
}
class _AddEntrySheetState extends State<_AddEntrySheet> {
final _query = TextEditingController();
/// Only Recipes of this Course; null shows all.
String? _courseId;
@override
void dispose() {
_query.dispose();
super.dispose();
}
Future<void> _add({String? recipeId, String? note}) async {
final plan = context.read<PlanRepository>();
Navigator.pop(context);
await plan.addEntry(widget.day, widget.meal.id, recipeId: recipeId, note: note);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final c = CookItColors.of(context);
final provider = context.watch<RecipeListProvider>();
final text = _query.text.trim();
final recipes = provider.allRecipes
.where((r) => _courseId == null || r.courseId == _courseId)
.where((r) => r.name.toLowerCase().contains(text.toLowerCase()))
.toList();
return Padding(
padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom),
child: SizedBox(
height: MediaQuery.sizeOf(context).height * 0.75,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
l10n.addEntryTitle(widget.meal.name, dayLabel(l10n, widget.day, dayOf(DateTime.now()))),
style: TextStyle(fontFamily: 'Nunito', fontWeight: FontWeight.w900, fontSize: 22, color: c.ink),
),
const SizedBox(height: 12),
TextField(
controller: _query,
autofocus: true,
textCapitalization: TextCapitalization.sentences,
textInputAction: TextInputAction.done,
onChanged: (_) => setState(() {}),
onSubmitted: (value) {
if (value.trim().isNotEmpty) _add(note: value.trim());
},
decoration: InputDecoration(
hintText: l10n.addEntryHint,
prefixIcon: Icon(Icons.search_rounded, color: c.green),
fillColor: c.background,
),
),
],
),
),
SizedBox(
height: 40,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 20),
children: [
for (final course in [null, ...provider.courses])
Padding(
padding: const EdgeInsets.only(right: 8),
child: ChoiceChip(
label: Text(course == null ? l10n.filterAll : course.label),
selected: _courseId == course?.id,
showCheckmark: false,
onSelected: (_) => setState(() => _courseId = course?.id),
),
),
],
),
),
const SizedBox(height: 8),
Expanded(
child: ListView(
children: [
if (text.isNotEmpty)
ListTile(
leading: CircleAvatar(
backgroundColor: c.tiles[0],
child: Icon(Icons.sticky_note_2_outlined, color: c.green),
),
title: Text(l10n.addAsNote(text), style: const TextStyle(fontWeight: FontWeight.w800)),
onTap: () => _add(note: text),
),
for (final recipe in recipes)
ListTile(
leading: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: SizedBox(
width: 44,
height: 44,
child: RecipePhoto(
photo: recipe.mainPhoto,
seed: recipe.id,
placeholderEmoji: provider.courseOf(recipe)?.emoji,
cacheWidth: 132,
),
),
),
title: Text(recipe.name),
subtitle: provider.courseOf(recipe) == null ? null : Text(provider.courseOf(recipe)!.name),
onTap: () => _add(recipeId: recipe.id),
),
],
),
),
],
),
),
);
}
}