mirror of
https://github.com/flutter/samples.git
synced 2025-11-08 13:58:47 +00:00
Adds ai_recipe_generation sample (#2242)
Adding the demo app from my I/O talk. Because AI. ## Pre-launch Checklist - [x] I read the [Flutter Style Guide] _recently_, and have followed its advice. - [x] I signed the [CLA]. - [x] I read the [Contributors Guide]. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] All existing and new tests are passing. --------- Co-authored-by: Brett Morgan <brett.morgan@gmail.com>
This commit is contained in:
110
ai_recipe_generation/lib/features/recipes/recipe_model.dart
Normal file
110
ai_recipe_generation/lib/features/recipes/recipe_model.dart
Normal file
@@ -0,0 +1,110 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:ai_recipe_generation/util/json_parsing.dart';
|
||||
import 'package:google_generative_ai/google_generative_ai.dart';
|
||||
|
||||
class Recipe {
|
||||
Recipe({
|
||||
required this.title,
|
||||
required this.id,
|
||||
required this.description,
|
||||
required this.ingredients,
|
||||
required this.instructions,
|
||||
required this.cuisine,
|
||||
required this.allergens,
|
||||
required this.servings,
|
||||
required this.nutritionInformation,
|
||||
this.rating = -1,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String title;
|
||||
final String description;
|
||||
final List<String> ingredients;
|
||||
final List<String> instructions;
|
||||
final String cuisine;
|
||||
final List<String> allergens;
|
||||
final String servings;
|
||||
final Map<String, dynamic> nutritionInformation;
|
||||
int rating;
|
||||
|
||||
factory Recipe.fromGeneratedContent(GenerateContentResponse content) {
|
||||
/// failures should be handled when the response is received
|
||||
assert(content.text != null);
|
||||
|
||||
final validJson = cleanJson(content.text!);
|
||||
final json = jsonDecode(validJson);
|
||||
|
||||
if (json
|
||||
case {
|
||||
"ingredients": List<dynamic> ingredients,
|
||||
"instructions": List<dynamic> instructions,
|
||||
"title": String title,
|
||||
"id": String id,
|
||||
"cuisine": String cuisine,
|
||||
"description": String description,
|
||||
"servings": String servings,
|
||||
"nutritionInformation": Map<String, dynamic> nutritionInformation,
|
||||
"allergens": List<dynamic> allergens,
|
||||
}) {
|
||||
return Recipe(
|
||||
id: id,
|
||||
title: title,
|
||||
ingredients: ingredients.map((i) => i.toString()).toList(),
|
||||
instructions: instructions.map((i) => i.toString()).toList(),
|
||||
nutritionInformation: nutritionInformation,
|
||||
allergens: allergens.map((i) => i.toString()).toList(),
|
||||
cuisine: cuisine,
|
||||
servings: servings,
|
||||
description: description);
|
||||
}
|
||||
|
||||
throw JsonUnsupportedObjectError(json);
|
||||
}
|
||||
|
||||
Map<String, Object?> toFirestore() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'instructions': instructions,
|
||||
'ingredients': ingredients,
|
||||
'cuisine': cuisine,
|
||||
'rating': rating,
|
||||
'allergens': allergens,
|
||||
'nutritionInformation': nutritionInformation,
|
||||
'servings': servings,
|
||||
'description': description,
|
||||
};
|
||||
}
|
||||
|
||||
factory Recipe.fromFirestore(Map<String, Object?> data) {
|
||||
if (data
|
||||
case {
|
||||
"ingredients": List<dynamic> ingredients,
|
||||
"instructions": List<dynamic> instructions,
|
||||
"title": String title,
|
||||
"id": String id,
|
||||
"cuisine": String cuisine,
|
||||
"description": String description,
|
||||
"servings": String servings,
|
||||
"nutritionInformation": Map<String, dynamic> nutritionInformation,
|
||||
"allergens": List<dynamic> allergens,
|
||||
"rating": int rating
|
||||
}) {
|
||||
return Recipe(
|
||||
id: id,
|
||||
title: title,
|
||||
ingredients: ingredients.map((i) => i.toString()).toList(),
|
||||
instructions: instructions.map((i) => i.toString()).toList(),
|
||||
nutritionInformation: nutritionInformation,
|
||||
allergens: allergens.map((i) => i.toString()).toList(),
|
||||
cuisine: cuisine,
|
||||
servings: servings,
|
||||
description: description,
|
||||
rating: rating,
|
||||
);
|
||||
}
|
||||
|
||||
throw "Malformed Firestore data";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import '../../services/firestore.dart';
|
||||
import 'recipe_model.dart';
|
||||
|
||||
class SavedRecipesViewModel extends ChangeNotifier {
|
||||
List<Recipe> recipes = [];
|
||||
|
||||
final recipePath = '/recipes';
|
||||
final firestore = FirebaseFirestore.instance;
|
||||
|
||||
SavedRecipesViewModel() {
|
||||
firestore.collection(recipePath).snapshots().listen((querySnapshot) {
|
||||
recipes = querySnapshot.docs.map((doc) {
|
||||
final data = doc.data();
|
||||
return Recipe.fromFirestore(data);
|
||||
}).toList();
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
void deleteRecipe(Recipe recipe) {
|
||||
FirestoreService.deleteRecipe(recipe);
|
||||
}
|
||||
|
||||
void updateRecipe(Recipe recipe) {
|
||||
FirestoreService.updateRecipe(recipe);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import 'package:ai_recipe_generation/features/recipes/recipes_view_model.dart';
|
||||
import 'package:ai_recipe_generation/features/recipes/widgets/recipe_fullscreen_dialog.dart';
|
||||
import 'package:ai_recipe_generation/theme.dart';
|
||||
import 'package:ai_recipe_generation/util/extensions.dart';
|
||||
import 'package:ai_recipe_generation/widgets/highlight_border_on_hover_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../widgets/marketplace_button_widget.dart';
|
||||
import '../../widgets/star_rating.dart';
|
||||
import 'recipe_model.dart';
|
||||
|
||||
class SavedRecipesScreen extends StatefulWidget {
|
||||
const SavedRecipesScreen({super.key, required this.canScroll});
|
||||
|
||||
final bool canScroll;
|
||||
|
||||
@override
|
||||
State<SavedRecipesScreen> createState() => _SavedRecipesScreenState();
|
||||
}
|
||||
|
||||
class _SavedRecipesScreenState extends State<SavedRecipesScreen>
|
||||
with TickerProviderStateMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final viewModel = context.watch<SavedRecipesViewModel>();
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Padding(
|
||||
padding: constraints.isMobile
|
||||
? const EdgeInsets.only(
|
||||
left: MarketplaceTheme.spacing7,
|
||||
right: MarketplaceTheme.spacing7,
|
||||
bottom: MarketplaceTheme.spacing7,
|
||||
top: MarketplaceTheme.spacing7,
|
||||
)
|
||||
: const EdgeInsets.only(
|
||||
left: MarketplaceTheme.spacing7,
|
||||
right: MarketplaceTheme.spacing7,
|
||||
bottom: MarketplaceTheme.spacing1,
|
||||
top: MarketplaceTheme.spacing7,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(MarketplaceTheme.defaultBorderRadius),
|
||||
topRight: Radius.circular(50),
|
||||
bottomRight:
|
||||
Radius.circular(MarketplaceTheme.defaultBorderRadius),
|
||||
bottomLeft: Radius.circular(MarketplaceTheme.defaultBorderRadius),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MarketplaceTheme.borderColor),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft:
|
||||
Radius.circular(MarketplaceTheme.defaultBorderRadius),
|
||||
topRight: Radius.circular(50),
|
||||
bottomRight:
|
||||
Radius.circular(MarketplaceTheme.defaultBorderRadius),
|
||||
bottomLeft:
|
||||
Radius.circular(MarketplaceTheme.defaultBorderRadius),
|
||||
),
|
||||
color: Colors.white,
|
||||
),
|
||||
child: constraints.isMobile
|
||||
? ListView.builder(
|
||||
physics: widget.canScroll
|
||||
? const PageScrollPhysics()
|
||||
: const NeverScrollableScrollPhysics(),
|
||||
itemCount: viewModel.recipes.length,
|
||||
itemBuilder: (context, idx) {
|
||||
final recipe = viewModel.recipes[idx];
|
||||
return Container(
|
||||
margin: EdgeInsets.only(top: idx == 0 ? 70 : 0),
|
||||
child: Align(
|
||||
heightFactor: .5,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: MarketplaceTheme.spacing7,
|
||||
vertical: MarketplaceTheme.spacing7,
|
||||
),
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width * .99,
|
||||
height: 200,
|
||||
child: _ListTile(
|
||||
constraints: constraints,
|
||||
key: Key('$idx-${recipe.hashCode}'),
|
||||
recipe: recipe,
|
||||
idx: idx,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: GridView.count(
|
||||
physics: widget.canScroll
|
||||
? const PageScrollPhysics()
|
||||
: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 3,
|
||||
childAspectRatio: 1.5,
|
||||
children: [
|
||||
...List.generate(viewModel.recipes.length, (idx) {
|
||||
final recipe = viewModel.recipes[idx];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: MarketplaceTheme.spacing7,
|
||||
vertical: MarketplaceTheme.spacing7,
|
||||
),
|
||||
child: _ListTile(
|
||||
key: Key('$idx-${recipe.hashCode}'),
|
||||
recipe: recipe,
|
||||
idx: idx,
|
||||
constraints: constraints,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ListTile extends StatefulWidget {
|
||||
const _ListTile({
|
||||
super.key,
|
||||
required this.recipe,
|
||||
this.idx = 0,
|
||||
required this.constraints,
|
||||
});
|
||||
|
||||
final Recipe recipe;
|
||||
final int idx;
|
||||
final BoxConstraints constraints;
|
||||
|
||||
@override
|
||||
State<_ListTile> createState() => _ListTileState();
|
||||
}
|
||||
|
||||
class _ListTileState extends State<_ListTile> {
|
||||
final List<Color> colors = [
|
||||
MarketplaceTheme.primary,
|
||||
MarketplaceTheme.secondary,
|
||||
MarketplaceTheme.tertiary,
|
||||
MarketplaceTheme.scrim,
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final viewModel = context.watch<SavedRecipesViewModel>();
|
||||
final color = colors[widget.idx % colors.length];
|
||||
|
||||
return GestureDetector(
|
||||
child: HighlightBorderOnHoverWidget(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(MarketplaceTheme.defaultBorderRadius),
|
||||
topRight: Radius.circular(50),
|
||||
bottomRight: Radius.circular(MarketplaceTheme.defaultBorderRadius),
|
||||
bottomLeft: Radius.circular(MarketplaceTheme.defaultBorderRadius),
|
||||
),
|
||||
color: color,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
offset: Offset(0, -2),
|
||||
color: Colors.black38,
|
||||
blurRadius: 5,
|
||||
),
|
||||
],
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(MarketplaceTheme.defaultBorderRadius),
|
||||
topRight: Radius.circular(50),
|
||||
bottomRight:
|
||||
Radius.circular(MarketplaceTheme.defaultBorderRadius),
|
||||
bottomLeft: Radius.circular(MarketplaceTheme.defaultBorderRadius),
|
||||
),
|
||||
color: Colors.white,
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(MarketplaceTheme.defaultBorderRadius),
|
||||
topRight: Radius.circular(50),
|
||||
bottomRight:
|
||||
Radius.circular(MarketplaceTheme.defaultBorderRadius),
|
||||
bottomLeft:
|
||||
Radius.circular(MarketplaceTheme.defaultBorderRadius),
|
||||
),
|
||||
color: color.withOpacity(.3),
|
||||
),
|
||||
padding: const EdgeInsets.all(MarketplaceTheme.spacing7),
|
||||
child: Stack(
|
||||
children: [
|
||||
Text(
|
||||
widget.recipe.title,
|
||||
style: MarketplaceTheme.heading3,
|
||||
),
|
||||
Positioned(
|
||||
top: widget.constraints.isMobile ? 40 : 60,
|
||||
left: 0,
|
||||
child: Text(
|
||||
widget.recipe.cuisine,
|
||||
style: MarketplaceTheme.subheading1,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 15,
|
||||
top: widget.constraints.isMobile ? 40 : 60,
|
||||
child: StartRating(
|
||||
initialRating: widget.recipe.rating,
|
||||
starColor: color,
|
||||
onTap: null,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
await showDialog<Null>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return RecipeDialogScreen(
|
||||
recipe: widget.recipe,
|
||||
subheading: Row(
|
||||
children: [
|
||||
const Text('My rating:'),
|
||||
const SizedBox(width: 10),
|
||||
StartRating(
|
||||
initialRating: widget.recipe.rating,
|
||||
starColor: MarketplaceTheme.tertiary,
|
||||
onTap: (index) {
|
||||
widget.recipe.rating = index + 1;
|
||||
viewModel.updateRecipe(widget.recipe);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
MarketplaceButton(
|
||||
onPressed: () {
|
||||
viewModel.deleteRecipe(widget.recipe);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
buttonText: "Delete Recipe",
|
||||
icon: Symbols.delete,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../../theme.dart';
|
||||
import '../recipe_model.dart';
|
||||
|
||||
class RecipeDisplayWidget extends StatelessWidget {
|
||||
const RecipeDisplayWidget({
|
||||
super.key,
|
||||
required this.recipe,
|
||||
this.subheading,
|
||||
});
|
||||
|
||||
final Recipe recipe;
|
||||
final Widget? subheading;
|
||||
|
||||
List<Widget> _buildIngredients(List<String> ingredients) {
|
||||
final widgets = <Widget>[];
|
||||
for (var ingredient in ingredients) {
|
||||
widgets.add(
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Symbols.stat_0_rounded,
|
||||
size: 12,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 5,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
ingredient,
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return widgets;
|
||||
}
|
||||
|
||||
List<Widget> _buildInstructions(List<String> instructions) {
|
||||
final widgets = <Widget>[];
|
||||
|
||||
// check for existing numbers in instructions.
|
||||
if (instructions.first.startsWith(RegExp('[0-9]'))) {
|
||||
for (var instruction in instructions) {
|
||||
widgets.add(Text(instruction));
|
||||
widgets.add(const SizedBox(height: MarketplaceTheme.spacing6));
|
||||
}
|
||||
} else {
|
||||
for (var i = 0; i < instructions.length; i++) {
|
||||
widgets.add(Text(
|
||||
'${i + 1}. ${instructions[i]}',
|
||||
softWrap: true,
|
||||
));
|
||||
widgets.add(const SizedBox(height: MarketplaceTheme.spacing6));
|
||||
}
|
||||
}
|
||||
|
||||
return widgets;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(MarketplaceTheme.defaultBorderRadius),
|
||||
color: MarketplaceTheme.primary.withOpacity(.5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
recipe.title,
|
||||
softWrap: true,
|
||||
style: MarketplaceTheme.heading2,
|
||||
),
|
||||
if (subheading != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: MarketplaceTheme.spacing7,
|
||||
),
|
||||
child: subheading,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateColor.resolveWith((states) {
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
return MarketplaceTheme.scrim.withOpacity(.6);
|
||||
}
|
||||
return Colors.white;
|
||||
}),
|
||||
shape: WidgetStateProperty.resolveWith(
|
||||
(states) {
|
||||
return RoundedRectangleBorder(
|
||||
side: const BorderSide(
|
||||
color: MarketplaceTheme.primary),
|
||||
borderRadius: BorderRadius.circular(
|
||||
MarketplaceTheme.defaultBorderRadius,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
textStyle: WidgetStateTextStyle.resolveWith(
|
||||
(states) {
|
||||
return MarketplaceTheme.dossierParagraph.copyWith(
|
||||
color: Colors.black45,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
await showDialog<dynamic>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.all(
|
||||
MarketplaceTheme.spacing7),
|
||||
child: Text(recipe.description),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Transform.translate(
|
||||
offset: const Offset(0, 5),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: MarketplaceTheme.spacing6),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 35,
|
||||
height: 35,
|
||||
child: SvgPicture.asset(
|
||||
'assets/chef_cat.svg',
|
||||
semanticsLabel: 'Chef cat icon',
|
||||
),
|
||||
),
|
||||
Transform.translate(
|
||||
offset: const Offset(1, -6),
|
||||
child: Transform.rotate(
|
||||
angle: -pi / 20.0,
|
||||
child: Text(
|
||||
'Chef Noodle \n says...',
|
||||
style: MarketplaceTheme.label,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
const Divider(
|
||||
height: 40,
|
||||
color: Colors.black26,
|
||||
),
|
||||
Table(
|
||||
columnWidths: const {
|
||||
0: FlexColumnWidth(2),
|
||||
1: FlexColumnWidth(3),
|
||||
},
|
||||
children: [
|
||||
TableRow(
|
||||
children: [
|
||||
Text(
|
||||
'Allergens:',
|
||||
style: MarketplaceTheme.paragraph.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(recipe.allergens.join(', '))
|
||||
],
|
||||
),
|
||||
TableRow(children: [
|
||||
Text(
|
||||
'Servings:',
|
||||
style: MarketplaceTheme.paragraph.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(recipe.servings)
|
||||
]),
|
||||
TableRow(children: [
|
||||
Text(
|
||||
'Nutrition per serving:',
|
||||
style: MarketplaceTheme.paragraph.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Text(''),
|
||||
]),
|
||||
...recipe.nutritionInformation.entries.map((entry) {
|
||||
return TableRow(children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Symbols.stat_0_rounded,
|
||||
size: 12,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 5,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
entry.key,
|
||||
style: MarketplaceTheme.label,
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(entry.value as String,
|
||||
style: MarketplaceTheme.label)
|
||||
]);
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
/// Body section
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(MarketplaceTheme.spacing4),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: MarketplaceTheme.spacing7,
|
||||
),
|
||||
child:
|
||||
Text('Ingredients:', style: MarketplaceTheme.subheading1),
|
||||
),
|
||||
..._buildIngredients(recipe.ingredients),
|
||||
const SizedBox(height: MarketplaceTheme.spacing4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: MarketplaceTheme.spacing7),
|
||||
child: Text('Instructions:',
|
||||
style: MarketplaceTheme.subheading1),
|
||||
),
|
||||
..._buildInstructions(recipe.instructions),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'package:ai_recipe_generation/features/recipes/widgets/recipe_display_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../../theme.dart';
|
||||
import '../../../widgets/marketplace_button_widget.dart';
|
||||
import '../recipe_model.dart';
|
||||
|
||||
class RecipeDialogScreen extends StatelessWidget {
|
||||
const RecipeDialogScreen({
|
||||
super.key,
|
||||
required this.recipe,
|
||||
required this.actions,
|
||||
this.subheading,
|
||||
});
|
||||
|
||||
final Recipe recipe;
|
||||
final List<Widget> actions;
|
||||
final Widget? subheading;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog.fullscreen(
|
||||
backgroundColor: Colors.white,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: RecipeDisplayWidget(
|
||||
recipe: recipe,
|
||||
subheading: subheading,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: MarketplaceTheme.spacing5,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
MarketplaceButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
buttonText: 'Close',
|
||||
icon: Symbols.close,
|
||||
),
|
||||
...actions,
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user