Initial commit: CookIT recipe collection and week planner
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
7d7a0bc60b
173 files changed
+22803
No files matched your search
@@ -0,0 +1,201 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../design/cookit_colors.dart';
|
||||
import '../../localization/app_localizations.dart';
|
||||
import '../../recipes/amount.dart';
|
||||
import '../../recipes/models/recipe.dart';
|
||||
import '../../recipes/time_format.dart';
|
||||
import '../../recipes/unit_catalog.dart';
|
||||
import '../../recipes/views/recipe_detail_view.dart';
|
||||
import '../recipe_share_service.dart';
|
||||
import '../shared_recipe.dart';
|
||||
|
||||
/// A Recipe from another cook, shown before anything is saved: what it holds,
|
||||
/// a hint when one of that name exists already, and "Speichern".
|
||||
class SharedRecipeView extends StatefulWidget {
|
||||
const SharedRecipeView({super.key, required this.shared});
|
||||
|
||||
static const routeName = '/received';
|
||||
|
||||
final SharedRecipe shared;
|
||||
|
||||
@override
|
||||
State<SharedRecipeView> createState() => _SharedRecipeViewState();
|
||||
}
|
||||
|
||||
class _SharedRecipeViewState extends State<SharedRecipeView> {
|
||||
late final Future<bool> _nameTaken = context.read<RecipeShareService>().hasRecipeNamed(widget.shared.recipe.name);
|
||||
var _saving = false;
|
||||
|
||||
Future<void> _save() async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final navigator = Navigator.of(context);
|
||||
setState(() => _saving = true);
|
||||
final recipe = await context.read<RecipeShareService>().save(widget.shared);
|
||||
navigator.pushReplacementNamed(RecipeDetailView.routeName, arguments: recipe.id);
|
||||
messenger
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(SnackBar(content: Text(l10n.recipeSaved(recipe.name))));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final c = CookItColors.of(context);
|
||||
final units = context.watch<UnitCatalog>();
|
||||
final shared = widget.shared;
|
||||
final recipe = shared.recipe;
|
||||
final sentUnits = {for (final u in shared.units) u.key: u.label};
|
||||
String unitLabel(String key) =>
|
||||
units.byKey(key) != null || !sentUnits.containsKey(key) ? units.label(l10n, key) : sentUnits[key]!;
|
||||
final facts = [
|
||||
if (shared.courseName != null) shared.courseName!,
|
||||
if (recipe.servings != null) '${formatNumber(recipe.servings!)} ${l10n.factServings}',
|
||||
if (recipe.totalMinutes != null) formatMinutes(l10n, recipe.totalMinutes!),
|
||||
].join(' · ');
|
||||
final heading = TextStyle(fontFamily: 'Nunito', fontWeight: FontWeight.w800, fontSize: 17, color: c.green);
|
||||
var number = 0;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(l10n.receivedRecipe)),
|
||||
bottomNavigationBar: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 16),
|
||||
child: SizedBox(
|
||||
height: 52,
|
||||
child: FilledButton.icon(
|
||||
onPressed: _saving ? null : _save,
|
||||
icon: const Icon(Icons.download_done_rounded),
|
||||
label: Text(l10n.save),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
|
||||
children: [
|
||||
if (shared.photos.isNotEmpty) ...[
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 10,
|
||||
child: Image.memory(shared.photos.first, fit: BoxFit.cover, cacheWidth: 1000),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
Text(
|
||||
recipe.name,
|
||||
style: TextStyle(fontFamily: 'Nunito', fontWeight: FontWeight.w900, fontSize: 28, color: c.ink),
|
||||
),
|
||||
if (facts.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
facts,
|
||||
style: TextStyle(fontWeight: FontWeight.w700, color: c.inkMuted),
|
||||
),
|
||||
],
|
||||
FutureBuilder<bool>(
|
||||
future: _nameTaken,
|
||||
builder: (context, snapshot) => snapshot.data != true
|
||||
? const SizedBox.shrink()
|
||||
: Container(
|
||||
margin: const EdgeInsets.only(top: 14),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(color: c.honey, borderRadius: BorderRadius.circular(14)),
|
||||
child: Text(
|
||||
l10n.recipeNameTaken(recipe.name),
|
||||
style: TextStyle(fontWeight: FontWeight.w700, color: c.onHoney),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(l10n.ingredients, style: heading),
|
||||
if (recipe.ingredientGroups.isEmpty) Text(l10n.noIngredients),
|
||||
for (final group in recipe.ingredientGroups) ...[
|
||||
if (group.name != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 10, bottom: 2),
|
||||
child: Text(
|
||||
group.name!,
|
||||
style: TextStyle(fontWeight: FontWeight.w800, color: c.ink),
|
||||
),
|
||||
),
|
||||
for (final line in group.lines) _IngredientLine(line: line, unitLabel: unitLabel),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
Text(l10n.steps, style: heading),
|
||||
if (recipe.stepSections.isEmpty) Text(l10n.noSteps),
|
||||
for (final section in recipe.stepSections) ...[
|
||||
if (section.title != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 10, bottom: 2),
|
||||
child: Text(
|
||||
section.title!,
|
||||
style: TextStyle(fontWeight: FontWeight.w800, color: c.ink),
|
||||
),
|
||||
),
|
||||
for (final step in section.steps)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Text('${++number}. $step', style: TextStyle(fontSize: 15, height: 1.4, color: c.ink)),
|
||||
),
|
||||
],
|
||||
if (recipe.notes != null) ...[
|
||||
const SizedBox(height: 20),
|
||||
Text(l10n.notes, style: heading),
|
||||
Text(recipe.notes!, style: TextStyle(fontSize: 15, height: 1.4, color: c.ink)),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _IngredientLine extends StatelessWidget {
|
||||
const _IngredientLine({required this.line, required this.unitLabel});
|
||||
|
||||
final IngredientLine line;
|
||||
final String Function(String key) unitLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final c = CookItColors.of(context);
|
||||
final amount = [
|
||||
formatAmount(line.amount, line.amountMax),
|
||||
if (line.unitKey != null) unitLabel(line.unitKey!),
|
||||
].where((s) => s.isNotEmpty).join(' ');
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 96,
|
||||
child: Text(
|
||||
amount,
|
||||
style: TextStyle(fontWeight: FontWeight.w800, color: c.ink),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
text: line.name,
|
||||
children: [
|
||||
if (line.remark != null)
|
||||
TextSpan(
|
||||
text: ' ${line.remark}',
|
||||
style: TextStyle(color: c.inkMuted),
|
||||
),
|
||||
],
|
||||
),
|
||||
style: TextStyle(color: c.ink),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user