initial commit
This commit is contained in:
@@ -0,0 +1,409 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_meals/models/category.dart';
|
||||
import 'package:flutter_meals/models/meal.dart';
|
||||
|
||||
// Constants in Dart should be written in lowerCamelcase.
|
||||
const availableCategories = [
|
||||
Category(
|
||||
id: 'c1',
|
||||
title: 'Italian',
|
||||
color: Colors.purple,
|
||||
),
|
||||
Category(
|
||||
id: 'c2',
|
||||
title: 'Quick & Easy',
|
||||
color: Colors.red,
|
||||
),
|
||||
Category(
|
||||
id: 'c3',
|
||||
title: 'Hamburgers',
|
||||
color: Colors.orange,
|
||||
),
|
||||
Category(
|
||||
id: 'c4',
|
||||
title: 'German',
|
||||
color: Colors.amber,
|
||||
),
|
||||
Category(
|
||||
id: 'c5',
|
||||
title: 'Light & Lovely',
|
||||
color: Colors.blue,
|
||||
),
|
||||
Category(
|
||||
id: 'c6',
|
||||
title: 'Exotic',
|
||||
color: Colors.green,
|
||||
),
|
||||
Category(
|
||||
id: 'c7',
|
||||
title: 'Breakfast',
|
||||
color: Colors.lightBlue,
|
||||
),
|
||||
Category(
|
||||
id: 'c8',
|
||||
title: 'Asian',
|
||||
color: Colors.lightGreen,
|
||||
),
|
||||
Category(
|
||||
id: 'c9',
|
||||
title: 'French',
|
||||
color: Colors.pink,
|
||||
),
|
||||
Category(
|
||||
id: 'c10',
|
||||
title: 'Summer',
|
||||
color: Colors.teal,
|
||||
),
|
||||
];
|
||||
|
||||
const dummyMeals = [
|
||||
Meal(
|
||||
id: 'm1',
|
||||
categories: [
|
||||
'c1',
|
||||
'c2',
|
||||
],
|
||||
title: 'Spaghetti with Tomato Sauce',
|
||||
affordability: Affordability.affordable,
|
||||
complexity: Complexity.simple,
|
||||
imageUrl:
|
||||
'https://upload.wikimedia.org/wikipedia/commons/thumb/2/20/Spaghetti_Bolognese_mit_Parmesan_oder_Grana_Padano.jpg/800px-Spaghetti_Bolognese_mit_Parmesan_oder_Grana_Padano.jpg',
|
||||
duration: 20,
|
||||
ingredients: [
|
||||
'4 Tomatoes',
|
||||
'1 Tablespoon of Olive Oil',
|
||||
'1 Onion',
|
||||
'250g Spaghetti',
|
||||
'Spices',
|
||||
'Cheese (optional)'
|
||||
],
|
||||
steps: [
|
||||
'Cut the tomatoes and the onion into small pieces.',
|
||||
'Boil some water - add salt to it once it boils.',
|
||||
'Put the spaghetti into the boiling water - they should be done in about 10 to 12 minutes.',
|
||||
'In the meantime, heaten up some olive oil and add the cut onion.',
|
||||
'After 2 minutes, add the tomato pieces, salt, pepper and your other spices.',
|
||||
'The sauce will be done once the spaghetti are.',
|
||||
'Feel free to add some cheese on top of the finished dish.'
|
||||
],
|
||||
isGlutenFree: false,
|
||||
isVegan: true,
|
||||
isVegetarian: true,
|
||||
isLactoseFree: true,
|
||||
),
|
||||
Meal(
|
||||
id: 'm2',
|
||||
categories: [
|
||||
'c2',
|
||||
],
|
||||
title: 'Toast Hawaii',
|
||||
affordability: Affordability.affordable,
|
||||
complexity: Complexity.simple,
|
||||
imageUrl:
|
||||
'https://cdn.pixabay.com/photo/2018/07/11/21/51/toast-3532016_1280.jpg',
|
||||
duration: 10,
|
||||
ingredients: [
|
||||
'1 Slice White Bread',
|
||||
'1 Slice Ham',
|
||||
'1 Slice Pineapple',
|
||||
'1-2 Slices of Cheese',
|
||||
'Butter'
|
||||
],
|
||||
steps: [
|
||||
'Butter one side of the white bread',
|
||||
'Layer ham, the pineapple and cheese on the white bread',
|
||||
'Bake the toast for round about 10 minutes in the oven at 200°C'
|
||||
],
|
||||
isGlutenFree: false,
|
||||
isVegan: false,
|
||||
isVegetarian: false,
|
||||
isLactoseFree: false,
|
||||
),
|
||||
Meal(
|
||||
id: 'm3',
|
||||
categories: [
|
||||
'c2',
|
||||
'c3',
|
||||
],
|
||||
title: 'Classic Hamburger',
|
||||
affordability: Affordability.pricey,
|
||||
complexity: Complexity.simple,
|
||||
imageUrl:
|
||||
'https://cdn.pixabay.com/photo/2014/10/23/18/05/burger-500054_1280.jpg',
|
||||
duration: 45,
|
||||
ingredients: [
|
||||
'300g Cattle Hack',
|
||||
'1 Tomato',
|
||||
'1 Cucumber',
|
||||
'1 Onion',
|
||||
'Ketchup',
|
||||
'2 Burger Buns'
|
||||
],
|
||||
steps: [
|
||||
'Form 2 patties',
|
||||
'Fry the patties for c. 4 minutes on each side',
|
||||
'Quickly fry the buns for c. 1 minute on each side',
|
||||
'Bruch buns with ketchup',
|
||||
'Serve burger with tomato, cucumber and onion'
|
||||
],
|
||||
isGlutenFree: false,
|
||||
isVegan: false,
|
||||
isVegetarian: false,
|
||||
isLactoseFree: true,
|
||||
),
|
||||
Meal(
|
||||
id: 'm4',
|
||||
categories: [
|
||||
'c4',
|
||||
],
|
||||
title: 'Wiener Schnitzel',
|
||||
affordability: Affordability.luxurious,
|
||||
complexity: Complexity.challenging,
|
||||
imageUrl:
|
||||
'https://cdn.pixabay.com/photo/2018/03/31/19/29/schnitzel-3279045_1280.jpg',
|
||||
duration: 60,
|
||||
ingredients: [
|
||||
'8 Veal Cutlets',
|
||||
'4 Eggs',
|
||||
'200g Bread Crumbs',
|
||||
'100g Flour',
|
||||
'300ml Butter',
|
||||
'100g Vegetable Oil',
|
||||
'Salt',
|
||||
'Lemon Slices'
|
||||
],
|
||||
steps: [
|
||||
'Tenderize the veal to about 2–4mm, and salt on both sides.',
|
||||
'On a flat plate, stir the eggs briefly with a fork.',
|
||||
'Lightly coat the cutlets in flour then dip into the egg, and finally, coat in breadcrumbs.',
|
||||
'Heat the butter and oil in a large pan (allow the fat to get very hot) and fry the schnitzels until golden brown on both sides.',
|
||||
'Make sure to toss the pan regularly so that the schnitzels are surrounded by oil and the crumbing becomes ‘fluffy’.',
|
||||
'Remove, and drain on kitchen paper. Fry the parsley in the remaining oil and drain.',
|
||||
'Place the schnitzels on awarmed plate and serve garnishedwith parsley and slices of lemon.'
|
||||
],
|
||||
isGlutenFree: false,
|
||||
isVegan: false,
|
||||
isVegetarian: false,
|
||||
isLactoseFree: false,
|
||||
),
|
||||
Meal(
|
||||
id: 'm5',
|
||||
categories: [
|
||||
'c2'
|
||||
'c5',
|
||||
'c10',
|
||||
],
|
||||
title: 'Salad with Smoked Salmon',
|
||||
affordability: Affordability.luxurious,
|
||||
complexity: Complexity.simple,
|
||||
imageUrl:
|
||||
'https://cdn.pixabay.com/photo/2016/10/25/13/29/smoked-salmon-salad-1768890_1280.jpg',
|
||||
duration: 15,
|
||||
ingredients: [
|
||||
'Arugula',
|
||||
'Lamb\'s Lettuce',
|
||||
'Parsley',
|
||||
'Fennel',
|
||||
'200g Smoked Salmon',
|
||||
'Mustard',
|
||||
'Balsamic Vinegar',
|
||||
'Olive Oil',
|
||||
'Salt and Pepper'
|
||||
],
|
||||
steps: [
|
||||
'Wash and cut salad and herbs',
|
||||
'Dice the salmon',
|
||||
'Process mustard, vinegar and olive oil into a dessing',
|
||||
'Prepare the salad',
|
||||
'Add salmon cubes and dressing'
|
||||
],
|
||||
isGlutenFree: true,
|
||||
isVegan: false,
|
||||
isVegetarian: true,
|
||||
isLactoseFree: true,
|
||||
),
|
||||
Meal(
|
||||
id: 'm6',
|
||||
categories: [
|
||||
'c6',
|
||||
'c10',
|
||||
],
|
||||
title: 'Delicious Orange Mousse',
|
||||
affordability: Affordability.affordable,
|
||||
complexity: Complexity.hard,
|
||||
imageUrl:
|
||||
'https://cdn.pixabay.com/photo/2017/05/01/05/18/pastry-2274750_1280.jpg',
|
||||
duration: 240,
|
||||
ingredients: [
|
||||
'4 Sheets of Gelatine',
|
||||
'150ml Orange Juice',
|
||||
'80g Sugar',
|
||||
'300g Yoghurt',
|
||||
'200g Cream',
|
||||
'Orange Peel',
|
||||
],
|
||||
steps: [
|
||||
'Dissolve gelatine in pot',
|
||||
'Add orange juice and sugar',
|
||||
'Take pot off the stove',
|
||||
'Add 2 tablespoons of yoghurt',
|
||||
'Stir gelatin under remaining yoghurt',
|
||||
'Cool everything down in the refrigerator',
|
||||
'Whip the cream and lift it under die orange mass',
|
||||
'Cool down again for at least 4 hours',
|
||||
'Serve with orange peel',
|
||||
],
|
||||
isGlutenFree: true,
|
||||
isVegan: false,
|
||||
isVegetarian: true,
|
||||
isLactoseFree: false,
|
||||
),
|
||||
Meal(
|
||||
id: 'm7',
|
||||
categories: [
|
||||
'c7',
|
||||
],
|
||||
title: 'Pancakes',
|
||||
affordability: Affordability.affordable,
|
||||
complexity: Complexity.simple,
|
||||
imageUrl:
|
||||
'https://cdn.pixabay.com/photo/2018/07/10/21/23/pancake-3529653_1280.jpg',
|
||||
duration: 20,
|
||||
ingredients: [
|
||||
'1 1/2 Cups all-purpose Flour',
|
||||
'3 1/2 Teaspoons Baking Powder',
|
||||
'1 Teaspoon Salt',
|
||||
'1 Tablespoon White Sugar',
|
||||
'1 1/4 cups Milk',
|
||||
'1 Egg',
|
||||
'3 Tablespoons Butter, melted',
|
||||
],
|
||||
steps: [
|
||||
'In a large bowl, sift together the flour, baking powder, salt and sugar.',
|
||||
'Make a well in the center and pour in the milk, egg and melted butter; mix until smooth.',
|
||||
'Heat a lightly oiled griddle or frying pan over medium high heat.',
|
||||
'Pour or scoop the batter onto the griddle, using approximately 1/4 cup for each pancake. Brown on both sides and serve hot.'
|
||||
],
|
||||
isGlutenFree: true,
|
||||
isVegan: false,
|
||||
isVegetarian: true,
|
||||
isLactoseFree: false,
|
||||
),
|
||||
Meal(
|
||||
id: 'm8',
|
||||
categories: [
|
||||
'c8',
|
||||
],
|
||||
title: 'Creamy Indian Chicken Curry',
|
||||
affordability: Affordability.pricey,
|
||||
complexity: Complexity.challenging,
|
||||
imageUrl:
|
||||
'https://cdn.pixabay.com/photo/2018/06/18/16/05/indian-food-3482749_1280.jpg',
|
||||
duration: 35,
|
||||
ingredients: [
|
||||
'4 Chicken Breasts',
|
||||
'1 Onion',
|
||||
'2 Cloves of Garlic',
|
||||
'1 Piece of Ginger',
|
||||
'4 Tablespoons Almonds',
|
||||
'1 Teaspoon Cayenne Pepper',
|
||||
'500ml Coconut Milk',
|
||||
],
|
||||
steps: [
|
||||
'Slice and fry the chicken breast',
|
||||
'Process onion, garlic and ginger into paste and sauté everything',
|
||||
'Add spices and stir fry',
|
||||
'Add chicken breast + 250ml of water and cook everything for 10 minutes',
|
||||
'Add coconut milk',
|
||||
'Serve with rice'
|
||||
],
|
||||
isGlutenFree: true,
|
||||
isVegan: false,
|
||||
isVegetarian: false,
|
||||
isLactoseFree: true,
|
||||
),
|
||||
Meal(
|
||||
id: 'm9',
|
||||
categories: [
|
||||
'c9',
|
||||
],
|
||||
title: 'Chocolate Souffle',
|
||||
affordability: Affordability.affordable,
|
||||
complexity: Complexity.hard,
|
||||
imageUrl:
|
||||
'https://cdn.pixabay.com/photo/2014/08/07/21/07/souffle-412785_1280.jpg',
|
||||
duration: 45,
|
||||
ingredients: [
|
||||
'1 Teaspoon melted Butter',
|
||||
'2 Tablespoons white Sugar',
|
||||
'2 Ounces 70% dark Chocolate, broken into pieces',
|
||||
'1 Tablespoon Butter',
|
||||
'1 Tablespoon all-purpose Flour',
|
||||
'4 1/3 tablespoons cold Milk',
|
||||
'1 Pinch Salt',
|
||||
'1 Pinch Cayenne Pepper',
|
||||
'1 Large Egg Yolk',
|
||||
'2 Large Egg Whites',
|
||||
'1 Pinch Cream of Tartar',
|
||||
'1 Tablespoon white Sugar',
|
||||
],
|
||||
steps: [
|
||||
'Preheat oven to 190°C. Line a rimmed baking sheet with parchment paper.',
|
||||
'Brush bottom and sides of 2 ramekins lightly with 1 teaspoon melted butter; cover bottom and sides right up to the rim.',
|
||||
'Add 1 tablespoon white sugar to ramekins. Rotate ramekins until sugar coats all surfaces.',
|
||||
'Place chocolate pieces in a metal mixing bowl.',
|
||||
'Place bowl over a pan of about 3 cups hot water over low heat.',
|
||||
'Melt 1 tablespoon butter in a skillet over medium heat. Sprinkle in flour. Whisk until flour is incorporated into butter and mixture thickens.',
|
||||
'Whisk in cold milk until mixture becomes smooth and thickens. Transfer mixture to bowl with melted chocolate.',
|
||||
'Add salt and cayenne pepper. Mix together thoroughly. Add egg yolk and mix to combine.',
|
||||
'Leave bowl above the hot (not simmering) water to keep chocolate warm while you whip the egg whites.',
|
||||
'Place 2 egg whites in a mixing bowl; add cream of tartar. Whisk until mixture begins to thicken and a drizzle from the whisk stays on the surface about 1 second before disappearing into the mix.',
|
||||
'Add 1/3 of sugar and whisk in. Whisk in a bit more sugar about 15 seconds.',
|
||||
'whisk in the rest of the sugar. Continue whisking until mixture is about as thick as shaving cream and holds soft peaks, 3 to 5 minutes.',
|
||||
'Transfer a little less than half of egg whites to chocolate.',
|
||||
'Mix until egg whites are thoroughly incorporated into the chocolate.',
|
||||
'Add the rest of the egg whites; gently fold into the chocolate with a spatula, lifting from the bottom and folding over.',
|
||||
'Stop mixing after the egg white disappears. Divide mixture between 2 prepared ramekins. Place ramekins on prepared baking sheet.',
|
||||
'Bake in preheated oven until scuffles are puffed and have risen above the top of the rims, 12 to 15 minutes.',
|
||||
],
|
||||
isGlutenFree: true,
|
||||
isVegan: false,
|
||||
isVegetarian: true,
|
||||
isLactoseFree: false,
|
||||
),
|
||||
Meal(
|
||||
id: 'm10',
|
||||
categories: [
|
||||
'c2',
|
||||
'c5',
|
||||
'c10',
|
||||
],
|
||||
title: 'Asparagus Salad with Cherry Tomatoes',
|
||||
affordability: Affordability.luxurious,
|
||||
complexity: Complexity.simple,
|
||||
imageUrl:
|
||||
'https://cdn.pixabay.com/photo/2018/04/09/18/26/asparagus-3304997_1280.jpg',
|
||||
duration: 30,
|
||||
ingredients: [
|
||||
'White and Green Asparagus',
|
||||
'30g Pine Nuts',
|
||||
'300g Cherry Tomatoes',
|
||||
'Salad',
|
||||
'Salt, Pepper and Olive Oil'
|
||||
],
|
||||
steps: [
|
||||
'Wash, peel and cut the asparagus',
|
||||
'Cook in salted water',
|
||||
'Salt and pepper the asparagus',
|
||||
'Roast the pine nuts',
|
||||
'Halve the tomatoes',
|
||||
'Mix with asparagus, salad and dressing',
|
||||
'Serve with Baguette'
|
||||
],
|
||||
isGlutenFree: true,
|
||||
isVegan: true,
|
||||
isVegetarian: true,
|
||||
isLactoseFree: true,
|
||||
),
|
||||
];
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Category {
|
||||
const Category({
|
||||
required this.id,
|
||||
required this.title,
|
||||
this.color = Colors.orange,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String title;
|
||||
final Color color;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
enum Complexity {
|
||||
simple,
|
||||
challenging,
|
||||
hard,
|
||||
}
|
||||
|
||||
enum Affordability {
|
||||
affordable,
|
||||
pricey,
|
||||
luxurious,
|
||||
}
|
||||
|
||||
class Meal {
|
||||
const Meal({
|
||||
required this.id,
|
||||
required this.categories,
|
||||
required this.title,
|
||||
required this.imageUrl,
|
||||
required this.ingredients,
|
||||
required this.steps,
|
||||
required this.duration,
|
||||
required this.complexity,
|
||||
required this.affordability,
|
||||
required this.isGlutenFree,
|
||||
required this.isLactoseFree,
|
||||
required this.isVegan,
|
||||
required this.isVegetarian,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final List<String> categories;
|
||||
final String title;
|
||||
final String imageUrl;
|
||||
final List<String> ingredients;
|
||||
final List<String> steps;
|
||||
final int duration;
|
||||
final Complexity complexity;
|
||||
final Affordability affordability;
|
||||
final bool isGlutenFree;
|
||||
final bool isLactoseFree;
|
||||
final bool isVegan;
|
||||
final bool isVegetarian;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter_meals/models/meal.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class FavoriteMealsNotifier extends StateNotifier<List<Meal>> {
|
||||
FavoriteMealsNotifier() : super([]);
|
||||
|
||||
bool toggleMealFavoriteStatus(Meal meal) {
|
||||
final mealIsFavorite = state.contains(meal);
|
||||
|
||||
if (mealIsFavorite) {
|
||||
state = state.where((m) => m.id != meal.id).toList();
|
||||
return false;
|
||||
} else {
|
||||
state = [...state, meal];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final favoriteMealsProvider =
|
||||
StateNotifierProvider<FavoriteMealsNotifier, List<Meal>>((ref) {
|
||||
return FavoriteMealsNotifier();
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
enum Filter {
|
||||
glutenFree,
|
||||
lactoseFree,
|
||||
vegetarian,
|
||||
vegan,
|
||||
}
|
||||
|
||||
class FiltersNotifier extends StateNotifier<Map<Filter, bool>> {
|
||||
FiltersNotifier()
|
||||
: super({
|
||||
Filter.glutenFree: false,
|
||||
Filter.lactoseFree: false,
|
||||
Filter.vegetarian: false,
|
||||
Filter.vegan: false
|
||||
});
|
||||
|
||||
void setFilters(Map<Filter, bool> chosenFilters) {
|
||||
state = chosenFilters;
|
||||
}
|
||||
|
||||
void setFilter(Filter filter, bool isActive) {
|
||||
state = {
|
||||
...state,
|
||||
filter: isActive,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
final filtersProvider =
|
||||
StateNotifierProvider<FiltersNotifier, Map<Filter, bool>>(
|
||||
(ref) => FiltersNotifier());
|
||||
@@ -0,0 +1,6 @@
|
||||
import 'package:flutter_meals/data/dummy_data.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
final mealsProvider = Provider((ref) {
|
||||
return dummyMeals;
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_meals/data/dummy_data.dart';
|
||||
import 'package:flutter_meals/models/meal.dart';
|
||||
import 'package:flutter_meals/widgets/category_grid_item.dart';
|
||||
import 'package:flutter_meals/screens/meals.dart';
|
||||
import 'package:flutter_meals/models/category.dart';
|
||||
|
||||
class CategoriesScreen extends StatelessWidget {
|
||||
const CategoriesScreen({
|
||||
super.key,
|
||||
required this.availableMeals,
|
||||
});
|
||||
|
||||
final List<Meal> availableMeals;
|
||||
|
||||
void _selectCategory(BuildContext context, Category category) {
|
||||
final filteredMeals = availableMeals
|
||||
.where((meal) => meal.categories.contains(category.id))
|
||||
.toList();
|
||||
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (ctx) => MealsScreen(
|
||||
title: category.title,
|
||||
meals: filteredMeals,
|
||||
),
|
||||
),
|
||||
); // Navigator.push(context, route)
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 3 / 2,
|
||||
crossAxisSpacing: 20,
|
||||
mainAxisSpacing: 20,
|
||||
),
|
||||
children: [
|
||||
// availableCategories.map((category) => CategoryGridItem(category: category)).toList()
|
||||
for (final category in availableCategories)
|
||||
CategoryGridItem(
|
||||
category: category,
|
||||
onSelectCategory: () {
|
||||
_selectCategory(context, category);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:flutter_meals/providers/filters_provider.dart';
|
||||
|
||||
// import 'package:flutter_meals/screens/tabs.dart';
|
||||
// import 'package:flutter_meals/widgets/main_drawer.dart';
|
||||
|
||||
class FiltersScreen extends ConsumerStatefulWidget {
|
||||
const FiltersScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<FiltersScreen> createState() {
|
||||
return _FiltersScreenState();
|
||||
}
|
||||
}
|
||||
|
||||
class _FiltersScreenState extends ConsumerState<FiltersScreen> {
|
||||
var _glutenFreeFilterSet = false;
|
||||
var _lactoseFreeFilterSet = false;
|
||||
var _vegetarianFilterSet = false;
|
||||
var _veganFilterSet = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final activeFilters = ref.read(filtersProvider);
|
||||
|
||||
_glutenFreeFilterSet = activeFilters[Filter.glutenFree]!;
|
||||
_lactoseFreeFilterSet = activeFilters[Filter.lactoseFree]!;
|
||||
_vegetarianFilterSet = activeFilters[Filter.vegetarian]!;
|
||||
_veganFilterSet = activeFilters[Filter.vegan]!;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Your Filters'),
|
||||
),
|
||||
// drawer: MainDrawer(
|
||||
// onSelectScreen: (identifier) {
|
||||
// Navigator.of(context).pop();
|
||||
// if (identifier == 'meals') {
|
||||
// Navigator.of(context).pushReplacement(
|
||||
// MaterialPageRoute(
|
||||
// builder: (ctx) => const TabsScreen(),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
body: WillPopScope(
|
||||
onWillPop: () async {
|
||||
ref.read(filtersProvider.notifier).setFilters({
|
||||
Filter.glutenFree: _glutenFreeFilterSet,
|
||||
Filter.lactoseFree: _lactoseFreeFilterSet,
|
||||
Filter.vegetarian: _vegetarianFilterSet,
|
||||
Filter.vegan: _veganFilterSet,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
SwitchListTile(
|
||||
value: _glutenFreeFilterSet,
|
||||
onChanged: (isChecked) {
|
||||
setState(() {
|
||||
_glutenFreeFilterSet = isChecked;
|
||||
});
|
||||
},
|
||||
title: Text(
|
||||
'Gluten-free',
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'Only include gluten-free meals.',
|
||||
style: Theme.of(context).textTheme.labelMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
activeColor: Theme.of(context).colorScheme.tertiary,
|
||||
contentPadding: const EdgeInsets.only(left: 34, right: 22),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: _lactoseFreeFilterSet,
|
||||
onChanged: (isChecked) {
|
||||
setState(() {
|
||||
_lactoseFreeFilterSet = isChecked;
|
||||
});
|
||||
},
|
||||
title: Text(
|
||||
'Lactose-free',
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'Only include lactose-free meals.',
|
||||
style: Theme.of(context).textTheme.labelMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
activeColor: Theme.of(context).colorScheme.tertiary,
|
||||
contentPadding: const EdgeInsets.only(left: 34, right: 22),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: _vegetarianFilterSet,
|
||||
onChanged: (isChecked) {
|
||||
setState(() {
|
||||
_vegetarianFilterSet = isChecked;
|
||||
});
|
||||
},
|
||||
title: Text(
|
||||
'Vegetarian',
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'Only include vegetarian meals.',
|
||||
style: Theme.of(context).textTheme.labelMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
activeColor: Theme.of(context).colorScheme.tertiary,
|
||||
contentPadding: const EdgeInsets.only(left: 34, right: 22),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: _veganFilterSet,
|
||||
onChanged: (isChecked) {
|
||||
setState(() {
|
||||
_veganFilterSet = isChecked;
|
||||
});
|
||||
},
|
||||
title: Text(
|
||||
'Vegan',
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'Only include vegan meals.',
|
||||
style: Theme.of(context).textTheme.labelMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
activeColor: Theme.of(context).colorScheme.tertiary,
|
||||
contentPadding: const EdgeInsets.only(left: 34, right: 22),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:flutter_meals/models/meal.dart';
|
||||
import 'package:flutter_meals/providers/favorites_provider.dart';
|
||||
|
||||
class MealDetailsScreen extends ConsumerWidget {
|
||||
const MealDetailsScreen({
|
||||
super.key,
|
||||
required this.meal,
|
||||
});
|
||||
|
||||
final Meal meal;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(meal.title), actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
final wasAdded = ref
|
||||
.read(favoriteMealsProvider.notifier)
|
||||
.toggleMealFavoriteStatus(meal);
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
wasAdded ? 'Meal added as a favorite.' : 'Meal removed.'),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.star),
|
||||
)
|
||||
]),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Image.network(
|
||||
meal.imageUrl,
|
||||
height: 300,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Text(
|
||||
'Ingredients',
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
for (final ingredient in meal.ingredients)
|
||||
Text(
|
||||
ingredient,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Steps',
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
for (final step in meal.steps)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Text(
|
||||
step,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_meals/models/meal.dart';
|
||||
import 'package:flutter_meals/screens/meal_details.dart';
|
||||
import 'package:flutter_meals/widgets/meal_item.dart';
|
||||
|
||||
class MealsScreen extends StatelessWidget {
|
||||
const MealsScreen({
|
||||
super.key,
|
||||
this.title,
|
||||
required this.meals,
|
||||
});
|
||||
|
||||
final String? title;
|
||||
final List<Meal> meals;
|
||||
|
||||
void selectMeal(BuildContext context, Meal meal) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (ctx) => MealDetailsScreen(
|
||||
meal: meal,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget content = Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Uh oh ... nothing here!',
|
||||
style: Theme.of(context).textTheme.headlineLarge!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Try selecting a different category!',
|
||||
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (meals.isNotEmpty) {
|
||||
content = ListView.builder(
|
||||
itemCount: meals.length,
|
||||
itemBuilder: (ctx, index) => MealItem(
|
||||
meal: meals[index],
|
||||
onSelectMeal: (meal) {
|
||||
selectMeal(context, meal);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (title == null) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(title!),
|
||||
),
|
||||
body: content,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:flutter_meals/screens/categories.dart';
|
||||
import 'package:flutter_meals/screens/filters.dart';
|
||||
import 'package:flutter_meals/screens/meals.dart';
|
||||
import 'package:flutter_meals/widgets/main_drawer.dart';
|
||||
import 'package:flutter_meals/providers/meals_provider.dart';
|
||||
import 'package:flutter_meals/providers/favorites_provider.dart';
|
||||
import 'package:flutter_meals/providers/filters_provider.dart';
|
||||
|
||||
const kInitialFilters = {
|
||||
Filter.glutenFree: false,
|
||||
Filter.lactoseFree: false,
|
||||
Filter.vegetarian: false,
|
||||
Filter.vegan: false
|
||||
};
|
||||
|
||||
class TabsScreen extends ConsumerStatefulWidget {
|
||||
const TabsScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<TabsScreen> createState() {
|
||||
return _TabsScreenState();
|
||||
}
|
||||
}
|
||||
|
||||
class _TabsScreenState extends ConsumerState<TabsScreen> {
|
||||
int _selectedPageIndex = 0;
|
||||
|
||||
void _selectPage(int index) {
|
||||
setState(() {
|
||||
_selectedPageIndex = index;
|
||||
});
|
||||
}
|
||||
|
||||
void _setScreen(String identifier) async {
|
||||
Navigator.of(context).pop();
|
||||
if (identifier == 'filters') {
|
||||
await Navigator.of(context).push<Map<Filter, bool>>(
|
||||
MaterialPageRoute(
|
||||
builder: (ctx) => const FiltersScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final meals = ref.watch(mealsProvider);
|
||||
final activeFilters = ref.watch(filtersProvider);
|
||||
final availableMeals = meals.where((meal) {
|
||||
if (activeFilters[Filter.glutenFree]! && !meal.isGlutenFree) {
|
||||
return false;
|
||||
}
|
||||
if (activeFilters[Filter.lactoseFree]! && !meal.isLactoseFree) {
|
||||
return false;
|
||||
}
|
||||
if (activeFilters[Filter.vegetarian]! && !meal.isVegetarian) {
|
||||
return false;
|
||||
}
|
||||
if (activeFilters[Filter.vegan]! && !meal.isVegan) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
|
||||
Widget activePage = CategoriesScreen(
|
||||
availableMeals: availableMeals,
|
||||
);
|
||||
var activePageTitle = 'Categories';
|
||||
|
||||
if (_selectedPageIndex == 1) {
|
||||
final favoritesMeals = ref.watch(favoriteMealsProvider);
|
||||
|
||||
activePage = MealsScreen(
|
||||
meals: favoritesMeals,
|
||||
);
|
||||
activePageTitle = 'Your Favorites';
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(activePageTitle),
|
||||
),
|
||||
drawer: MainDrawer(
|
||||
onSelectScreen: _setScreen,
|
||||
),
|
||||
body: activePage,
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
onTap: _selectPage,
|
||||
currentIndex: _selectedPageIndex,
|
||||
items: const [
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.set_meal),
|
||||
label: 'Categories',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.star),
|
||||
label: 'Favorites',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_meals/models/category.dart';
|
||||
|
||||
class CategoryGridItem extends StatelessWidget {
|
||||
const CategoryGridItem({
|
||||
super.key,
|
||||
required this.category,
|
||||
required this.onSelectCategory,
|
||||
});
|
||||
|
||||
final Category category;
|
||||
final void Function() onSelectCategory;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onSelectCategory,
|
||||
splashColor: Theme.of(context).primaryColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
category.color.withOpacity(0.55),
|
||||
category.color.withOpacity(0.9),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
)),
|
||||
child: Text(
|
||||
category.title,
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MainDrawer extends StatelessWidget {
|
||||
const MainDrawer({super.key, required this.onSelectScreen});
|
||||
|
||||
final void Function(String identifier) onSelectScreen;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Drawer(
|
||||
child: Column(
|
||||
children: [
|
||||
DrawerHeader(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.primaryContainer,
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.primaryContainer
|
||||
.withOpacity(0.8),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.fastfood,
|
||||
size: 48,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 18),
|
||||
Text(
|
||||
'Cooking Up!',
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Icons.restaurant,
|
||||
size: 26,
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
title: Text(
|
||||
'Meals',
|
||||
style: Theme.of(context).textTheme.titleSmall!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
fontSize: 24,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
onSelectScreen('meals');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Icons.settings,
|
||||
size: 26,
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
title: Text(
|
||||
'Filters',
|
||||
style: Theme.of(context).textTheme.titleSmall!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
fontSize: 24,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
onSelectScreen('filters');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:transparent_image/transparent_image.dart';
|
||||
|
||||
import 'package:flutter_meals/widgets/meal_item_trait.dart';
|
||||
import 'package:flutter_meals/models/meal.dart';
|
||||
|
||||
class MealItem extends StatelessWidget {
|
||||
const MealItem({
|
||||
super.key,
|
||||
required this.meal,
|
||||
required this.onSelectMeal,
|
||||
});
|
||||
|
||||
final Meal meal;
|
||||
final void Function(Meal meal) onSelectMeal;
|
||||
|
||||
String get complexityText {
|
||||
return meal.complexity.name[0].toUpperCase() +
|
||||
meal.complexity.name.substring(1);
|
||||
}
|
||||
|
||||
String get affordabilityText {
|
||||
return meal.affordability.name[0].toUpperCase() +
|
||||
meal.affordability.name.substring(1);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
elevation: 2,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
onSelectMeal(meal);
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
FadeInImage(
|
||||
placeholder: MemoryImage(kTransparentImage),
|
||||
image: NetworkImage(meal.imageUrl),
|
||||
fit: BoxFit.cover,
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
color: Colors.black54,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 6, horizontal: 44),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
meal.title,
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis, // Very long text ...
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
MealItemTrait(
|
||||
icon: Icons.schedule,
|
||||
label: '${meal.duration} min',
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
MealItemTrait(
|
||||
icon: Icons.work,
|
||||
label: complexityText,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
MealItemTrait(
|
||||
icon: Icons.attach_money,
|
||||
label: affordabilityText,
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MealItemTrait extends StatelessWidget {
|
||||
const MealItemTrait({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 17,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user