pesto_demo.dart 25.1 KB
Newer Older
1 2 3 4 5
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';
6
import 'package:flutter/rendering.dart';
7

8
class PestoDemo extends StatelessWidget {
9
  const PestoDemo({ Key key }) : super(key: key);
10 11 12 13

  static const String routeName = '/pesto';

  @override
14
  Widget build(BuildContext context) => PestoHome();
15 16
}

17

18
const String _kSmallLogoImage = 'logos/pesto/logo_small.png';
19
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
20
const double _kAppBarHeight = 128.0;
21
const double _kFabHalfSize = 28.0; // TODO(mpcomplete): needs to adapt to screen size
22 23
const double _kRecipePageMaxWidth = 500.0;

24
final Set<Recipe> _favoriteRecipes = Set<Recipe>();
25

26
final ThemeData _kTheme = ThemeData(
27
  brightness: Brightness.light,
28
  primarySwatch: Colors.teal,
29
  accentColor: Colors.redAccent,
30 31
);

32 33 34
class PestoHome extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
35
    return const RecipeGridPage(recipes: kPestoRecipes);
36 37 38 39 40 41
  }
}

class PestoFavorites extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
42
    return RecipeGridPage(recipes: _favoriteRecipes.toList());
43 44
  }
}
45

46 47
class PestoStyle extends TextStyle {
  const PestoStyle({
48
    double fontSize = 12.0,
49
    FontWeight fontWeight,
50
    Color color = Colors.black87,
51 52
    double letterSpacing,
    double height,
53
  }) : super(
54
    inherit: false,
55
    color: color,
56
    fontFamily: 'Raleway',
57 58 59
    fontSize: fontSize,
    fontWeight: fontWeight,
    textBaseline: TextBaseline.alphabetic,
60 61
    letterSpacing: letterSpacing,
    height: height,
62 63 64
  );
}

65 66
// Displays a grid of recipe cards.
class RecipeGridPage extends StatefulWidget {
67
  const RecipeGridPage({ Key key, this.recipes }) : super(key: key);
68

69
  final List<Recipe> recipes;
70 71

  @override
72
  _RecipeGridPageState createState() => _RecipeGridPageState();
73 74
}

75
class _RecipeGridPageState extends State<RecipeGridPage> {
76
  final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
77 78 79

  @override
  Widget build(BuildContext context) {
80
    final double statusBarHeight = MediaQuery.of(context).padding.top;
81
    return Theme(
82
      data: _kTheme.copyWith(platform: Theme.of(context).platform),
83
      child: Scaffold(
84
        key: scaffoldKey,
85
        floatingActionButton: FloatingActionButton(
86
          child: const Icon(Icons.edit),
87
          onPressed: () {
88
            scaffoldKey.currentState.showSnackBar(const SnackBar(
89
              content: Text('Not supported.'),
90
            ));
91
          },
92
        ),
93
        body: CustomScrollView(
94
          semanticChildCount: widget.recipes.length,
95 96 97 98 99
          slivers: <Widget>[
            _buildAppBar(context, statusBarHeight),
            _buildBody(context, statusBarHeight),
          ],
        ),
100
      ),
101 102 103
    );
  }

104
  Widget _buildAppBar(BuildContext context, double statusBarHeight) {
105
    return SliverAppBar(
106
      pinned: true,
107
      expandedHeight: _kAppBarHeight,
108
      actions: <Widget>[
109
        IconButton(
110
          icon: const Icon(Icons.search),
111 112
          tooltip: 'Search',
          onPressed: () {
113
            scaffoldKey.currentState.showSnackBar(const SnackBar(
114
              content: Text('Not supported.'),
115
            ));
116 117
          },
        ),
118
      ],
119
      flexibleSpace: LayoutBuilder(
120 121
        builder: (BuildContext context, BoxConstraints constraints) {
          final Size size = constraints.biggest;
122
          final double appBarHeight = size.height - statusBarHeight;
123
          final double t = (appBarHeight - kToolbarHeight) / (_kAppBarHeight - kToolbarHeight);
124
          final double extraPadding = Tween<double>(begin: 10.0, end: 24.0).transform(t);
125
          final double logoHeight = appBarHeight - 1.5 * extraPadding;
126 127
          return Padding(
            padding: EdgeInsets.only(
128
              top: statusBarHeight + 0.5 * extraPadding,
129
              bottom: extraPadding,
130
            ),
131 132
            child: Center(
              child: PestoLogo(height: logoHeight, t: t.clamp(0.0, 1.0))
133
            ),
134
          );
135 136
        },
      ),
137 138 139
    );
  }

140
  Widget _buildBody(BuildContext context, double statusBarHeight) {
141
    final EdgeInsets mediaPadding = MediaQuery.of(context).padding;
142
    final EdgeInsets padding = EdgeInsets.only(
143 144 145 146 147
      top: 8.0,
      left: 8.0 + mediaPadding.left,
      right: 8.0 + mediaPadding.right,
      bottom: 8.0
    );
148
    return SliverPadding(
149
      padding: padding,
150
      sliver: SliverGrid(
151
        gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
152 153 154 155
          maxCrossAxisExtent: _kRecipePageMaxWidth,
          crossAxisSpacing: 8.0,
          mainAxisSpacing: 8.0,
        ),
156
        delegate: SliverChildBuilderDelegate(
157
          (BuildContext context, int index) {
158
            final Recipe recipe = widget.recipes[index];
159
            return RecipeCard(
160 161 162
              recipe: recipe,
              onTap: () { showRecipePage(context, recipe); },
            );
Ian Hickson's avatar
Ian Hickson committed
163
          },
164
          childCount: widget.recipes.length,
165
        ),
166 167 168 169
      ),
    );
  }

170
  void showFavoritesPage(BuildContext context) {
171
    Navigator.push(context, MaterialPageRoute<void>(
172
      settings: const RouteSettings(name: '/pesto/favorites'),
173
      builder: (BuildContext context) => PestoFavorites(),
174 175 176
    ));
  }

177
  void showRecipePage(BuildContext context, Recipe recipe) {
178
    Navigator.push(context, MaterialPageRoute<void>(
179
      settings: const RouteSettings(name: '/pesto/recipe'),
180
      builder: (BuildContext context) {
181
        return Theme(
182
          data: _kTheme.copyWith(platform: Theme.of(context).platform),
183
          child: RecipePage(recipe: recipe),
184
        );
185
      },
186 187
    ));
  }
188 189 190
}

class PestoLogo extends StatefulWidget {
191
  const PestoLogo({this.height, this.t});
192 193

  final double height;
194
  final double t;
195 196

  @override
197
  _PestoLogoState createState() => _PestoLogoState();
198 199 200 201 202 203 204 205 206
}

class _PestoLogoState extends State<PestoLogo> {
  // Native sizes for logo and its image/text components.
  static const double kLogoHeight = 162.0;
  static const double kLogoWidth = 220.0;
  static const double kImageHeight = 108.0;
  static const double kTextHeight = 48.0;
  final TextStyle titleStyle = const PestoStyle(fontSize: kTextHeight, fontWeight: FontWeight.w900, color: Colors.white, letterSpacing: 3.0);
207 208 209
  final RectTween _textRectTween = RectTween(
    begin: Rect.fromLTWH(0.0, kLogoHeight, kLogoWidth, kTextHeight),
    end: Rect.fromLTWH(0.0, kImageHeight, kLogoWidth, kTextHeight)
210
  );
211
  final Curve _textOpacity = const Interval(0.4, 1.0, curve: Curves.easeInOut);
212 213 214
  final RectTween _imageRectTween = RectTween(
    begin: Rect.fromLTWH(0.0, 0.0, kLogoWidth, kLogoHeight),
    end: Rect.fromLTWH(0.0, 0.0, kLogoWidth, kImageHeight),
215
  );
216 217 218

  @override
  Widget build(BuildContext context) {
219
    return Semantics(
220
      namesRoute: true,
221 222
      child: Transform(
        transform: Matrix4.identity()..scale(widget.height / kLogoHeight),
223
        alignment: Alignment.topCenter,
224
        child: SizedBox(
225
          width: kLogoWidth,
226
          child: Stack(
227 228
            overflow: Overflow.visible,
            children: <Widget>[
229
              Positioned.fromRect(
230
                rect: _imageRectTween.lerp(widget.t),
231
                child: Image.asset(
232 233 234 235
                  _kSmallLogoImage,
                  package: _kGalleryAssetsPackage,
                  fit: BoxFit.contain,
                ),
236
              ),
237
              Positioned.fromRect(
238
                rect: _textRectTween.lerp(widget.t),
239
                child: Opacity(
240
                  opacity: _textOpacity.transform(widget.t),
241
                  child: Text('PESTO', style: titleStyle, textAlign: TextAlign.center),
242
                ),
243
              ),
244 245
            ],
          ),
246 247
        ),
      ),
248 249
    );
  }
250 251
}

252 253
// A card with the recipe's image, author, and title.
class RecipeCard extends StatelessWidget {
254
  const RecipeCard({ Key key, this.recipe, this.onTap }) : super(key: key);
255 256 257 258

  final Recipe recipe;
  final VoidCallback onTap;

259 260 261
  TextStyle get titleStyle => const PestoStyle(fontSize: 24.0, fontWeight: FontWeight.w600);
  TextStyle get authorStyle => const PestoStyle(fontWeight: FontWeight.w500, color: Colors.black54);

262 263
  @override
  Widget build(BuildContext context) {
264
    return GestureDetector(
265
      onTap: onTap,
266 267
      child: Card(
        child: Column(
268 269
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
270
            Hero(
271
              tag: 'packages/$_kGalleryAssetsPackage/${recipe.imagePath}',
272 273 274 275 276 277 278 279
              child: AspectRatio(
                aspectRatio: 4.0 / 3.0,
                child: Image.asset(
                  recipe.imagePath,
                  package: recipe.imagePackage,
                  fit: BoxFit.cover,
                  semanticLabel: recipe.name,
                ),
280
              ),
281
            ),
282 283
            Expanded(
              child: Row(
284
                children: <Widget>[
285
                  Padding(
286
                    padding: const EdgeInsets.all(16.0),
287
                    child: Image.asset(
288 289 290 291
                      recipe.ingredientsImagePath,
                      package: recipe.ingredientsImagePackage,
                      width: 48.0,
                      height: 48.0,
292
                    ),
293
                  ),
294 295
                  Expanded(
                    child: Column(
296 297 298
                      crossAxisAlignment: CrossAxisAlignment.start,
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: <Widget>[
299 300
                        Text(recipe.name, style: titleStyle, softWrap: false, overflow: TextOverflow.ellipsis),
                        Text(recipe.author, style: authorStyle),
301
                      ],
302
                    ),
303 304
                  ),
                ],
305
              ),
306 307
            ),
          ],
308 309
        ),
      ),
310 311 312 313
    );
  }
}

314 315
// Displays one recipe. Includes the recipe sheet with a background image.
class RecipePage extends StatefulWidget {
316
  const RecipePage({ Key key, this.recipe }) : super(key: key);
317 318 319 320

  final Recipe recipe;

  @override
321
  _RecipePageState createState() => _RecipePageState();
322 323
}

324
class _RecipePageState extends State<RecipePage> {
325
  final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
326
  final TextStyle menuItemStyle = const PestoStyle(fontSize: 15.0, color: Colors.black54, height: 24.0/15.0);
327

328 329
  double _getAppBarHeight(BuildContext context) => MediaQuery.of(context).size.height * 0.3;

330 331
  @override
  Widget build(BuildContext context) {
332 333 334 335
    // The full page content with the recipe's image behind it. This
    // adjusts based on the size of the screen. If the recipe sheet touches
    // the edge of the screen, use a slightly different layout.
    final double appBarHeight = _getAppBarHeight(context);
336
    final Size screenSize = MediaQuery.of(context).size;
337
    final bool fullWidth = screenSize.width < _kRecipePageMaxWidth;
338
    final bool isFavorite = _favoriteRecipes.contains(widget.recipe);
339
    return Scaffold(
340
      key: _scaffoldKey,
341
      body: Stack(
342
        children: <Widget>[
343
          Positioned(
344 345 346 347
            top: 0.0,
            left: 0.0,
            right: 0.0,
            height: appBarHeight + _kFabHalfSize,
348
            child: Hero(
349
              tag: 'packages/$_kGalleryAssetsPackage/${widget.recipe.imagePath}',
350
              child: Image.asset(
351
                widget.recipe.imagePath,
352
                package: widget.recipe.imagePackage,
353
                fit: fullWidth ? BoxFit.fitWidth : BoxFit.cover,
354
              ),
355 356
            ),
          ),
357
          CustomScrollView(
358
            slivers: <Widget>[
359
              SliverAppBar(
360 361 362
                expandedHeight: appBarHeight - _kFabHalfSize,
                backgroundColor: Colors.transparent,
                actions: <Widget>[
363
                  PopupMenuButton<String>(
364 365 366 367 368 369 370 371 372
                    onSelected: (String item) {},
                    itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
                      _buildMenuItem(Icons.share, 'Tweet recipe'),
                      _buildMenuItem(Icons.email, 'Email recipe'),
                      _buildMenuItem(Icons.message, 'Message recipe'),
                      _buildMenuItem(Icons.people, 'Share on Facebook'),
                    ],
                  ),
                ],
373
                flexibleSpace: const FlexibleSpaceBar(
374 375 376 377 378 379
                  background: DecoratedBox(
                    decoration: BoxDecoration(
                      gradient: LinearGradient(
                        begin: Alignment(0.0, -1.0),
                        end: Alignment(0.0, -0.2),
                        colors: <Color>[Color(0x60000000), Color(0x00000000)],
380 381 382 383 384
                      ),
                    ),
                  ),
                ),
              ),
385 386
              SliverToBoxAdapter(
                child: Stack(
387
                  children: <Widget>[
388
                    Container(
389
                      padding: const EdgeInsets.only(top: _kFabHalfSize),
390
                      width: fullWidth ? null : _kRecipePageMaxWidth,
391
                      child: RecipeSheet(recipe: widget.recipe),
392
                    ),
393
                    Positioned(
394
                      right: 16.0,
395 396
                      child: FloatingActionButton(
                        child: Icon(isFavorite ? Icons.favorite : Icons.favorite_border),
397 398 399 400
                        onPressed: _toggleFavorite,
                      ),
                    ),
                  ],
401
                )
402
              ),
403
            ],
404
          ),
405 406
        ],
      ),
407 408 409 410
    );
  }

  PopupMenuItem<String> _buildMenuItem(IconData icon, String label) {
411 412
    return PopupMenuItem<String>(
      child: Row(
413
        children: <Widget>[
414
          Padding(
415
            padding: const EdgeInsets.only(right: 24.0),
416
            child: Icon(icon, color: Colors.black54)
417
          ),
418
          Text(label, style: menuItemStyle),
419 420
        ],
      ),
421 422 423 424 425
    );
  }

  void _toggleFavorite() {
    setState(() {
426 427
      if (_favoriteRecipes.contains(widget.recipe))
        _favoriteRecipes.remove(widget.recipe);
428
      else
429
        _favoriteRecipes.add(widget.recipe);
430 431 432 433
    });
  }
}

434 435
/// Displays the recipe's name and instructions.
class RecipeSheet extends StatelessWidget {
436 437
  RecipeSheet({ Key key, this.recipe }) : super(key: key);

438 439 440
  final TextStyle titleStyle = const PestoStyle(fontSize: 34.0);
  final TextStyle descriptionStyle = const PestoStyle(fontSize: 15.0, color: Colors.black54, height: 24.0/15.0);
  final TextStyle itemStyle = const PestoStyle(fontSize: 15.0, height: 24.0/15.0);
441
  final TextStyle itemAmountStyle = PestoStyle(fontSize: 15.0, color: _kTheme.primaryColor, height: 24.0/15.0);
442
  final TextStyle headingStyle = const PestoStyle(fontSize: 16.0, fontWeight: FontWeight.bold, height: 24.0/15.0);
443 444 445 446 447

  final Recipe recipe;

  @override
  Widget build(BuildContext context) {
448 449
    return Material(
      child: SafeArea(
450 451
        top: false,
        bottom: false,
452
        child: Padding(
453
          padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 40.0),
454
          child: Table(
455
            columnWidths: const <int, TableColumnWidth>{
456
              0: FixedColumnWidth(64.0)
457 458
            },
            children: <TableRow>[
459
              TableRow(
460
                children: <Widget>[
461
                  TableCell(
462
                    verticalAlignment: TableCellVerticalAlignment.middle,
463
                    child: Image.asset(
464 465 466 467 468 469 470 471
                      recipe.ingredientsImagePath,
                      package: recipe.ingredientsImagePackage,
                      width: 32.0,
                      height: 32.0,
                      alignment: Alignment.centerLeft,
                      fit: BoxFit.scaleDown
                    )
                  ),
472
                  TableCell(
473
                    verticalAlignment: TableCellVerticalAlignment.middle,
474
                    child: Text(recipe.name, style: titleStyle)
475 476 477
                  ),
                ]
              ),
478
              TableRow(
479 480
                children: <Widget>[
                  const SizedBox(),
481
                  Padding(
482
                    padding: const EdgeInsets.only(top: 8.0, bottom: 4.0),
483
                    child: Text(recipe.description, style: descriptionStyle)
484 485 486
                  ),
                ]
              ),
487
              TableRow(
488 489
                children: <Widget>[
                  const SizedBox(),
490
                  Padding(
491
                    padding: const EdgeInsets.only(top: 24.0, bottom: 4.0),
492
                    child: Text('Ingredients', style: headingStyle)
493 494 495
                  ),
                ]
              ),
496
            ]..addAll(recipe.ingredients.map<TableRow>(
497 498 499 500
              (RecipeIngredient ingredient) {
                return _buildItemRow(ingredient.amount, ingredient.description);
              }
            ))..add(
501
              TableRow(
502 503
                children: <Widget>[
                  const SizedBox(),
504
                  Padding(
505
                    padding: const EdgeInsets.only(top: 24.0, bottom: 4.0),
506
                    child: Text('Steps', style: headingStyle)
507 508 509
                  ),
                ]
              )
510
            )..addAll(recipe.steps.map<TableRow>(
511 512 513 514 515
              (RecipeStep step) {
                return _buildItemRow(step.duration ?? '', step.description);
              }
            )),
          ),
516 517
        ),
      ),
518 519 520 521
    );
  }

  TableRow _buildItemRow(String left, String right) {
522
    return TableRow(
523
      children: <Widget>[
524
        Padding(
525
          padding: const EdgeInsets.symmetric(vertical: 4.0),
526
          child: Text(left, style: itemAmountStyle),
527
        ),
528
        Padding(
529
          padding: const EdgeInsets.symmetric(vertical: 4.0),
530
          child: Text(right, style: itemStyle),
531 532
        ),
      ],
533 534 535 536 537 538 539 540 541 542
    );
  }
}

class Recipe {
  const Recipe({
    this.name,
    this.author,
    this.description,
    this.imagePath,
543
    this.imagePackage,
544
    this.ingredientsImagePath,
545
    this.ingredientsImagePackage,
546 547 548 549 550 551 552 553
    this.ingredients,
    this.steps
  });

  final String name;
  final String author;
  final String description;
  final String imagePath;
554
  final String imagePackage;
555
  final String ingredientsImagePath;
556
  final String ingredientsImagePackage;
557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574
  final List<RecipeIngredient> ingredients;
  final List<RecipeStep> steps;
}

class RecipeIngredient {
  const RecipeIngredient({this.amount, this.description});

  final String amount;
  final String description;
}

class RecipeStep {
  const RecipeStep({this.duration, this.description});

  final String duration;
  final String description;
}

575 576
const List<Recipe> kPestoRecipes = <Recipe>[
  Recipe(
577
    name: 'Roasted Chicken',
578
    author: 'Peter Carlsson',
579
    ingredientsImagePath: 'food/icons/main.png',
580
    ingredientsImagePackage: _kGalleryAssetsPackage,
581 582
    description: 'The perfect dish to welcome your family and friends with on a crisp autumn night. Pair with roasted veggies to truly impress them.',
    imagePath: 'food/roasted_chicken.png',
583
    imagePackage: _kGalleryAssetsPackage,
584
    ingredients: <RecipeIngredient>[
585 586 587 588
      RecipeIngredient(amount: '1 whole', description: 'Chicken'),
      RecipeIngredient(amount: '1/2 cup', description: 'Butter'),
      RecipeIngredient(amount: '1 tbsp', description: 'Onion powder'),
      RecipeIngredient(amount: '1 tbsp', description: 'Freshly ground pepper'),
589
      RecipeIngredient(amount: '1 tsp', description: 'Salt'),
590
    ],
591
    steps: <RecipeStep>[
592 593
      RecipeStep(duration: '1 min', description: 'Put in oven'),
      RecipeStep(duration: '1hr 45 min', description: 'Cook'),
594
    ],
595
  ),
596
  Recipe(
597
    name: 'Chopped Beet Leaves',
598
    author: 'Trevor Hansen',
599
    ingredientsImagePath: 'food/icons/veggie.png',
600
    ingredientsImagePackage: _kGalleryAssetsPackage,
601 602
    description: 'This vegetable has more to offer than just its root. Beet greens can be tossed into a salad to add some variety or sauteed on its own with some oil and garlic.',
    imagePath: 'food/chopped_beet_leaves.png',
603
    imagePackage: _kGalleryAssetsPackage,
604
    ingredients: <RecipeIngredient>[
605
       RecipeIngredient(amount: '3 cups', description: 'Beet greens'),
606
    ],
607
    steps: <RecipeStep>[
608
      RecipeStep(duration: '5 min', description: 'Chop'),
609
    ],
610
  ),
611
  Recipe(
612
    name: 'Pesto Pasta',
613
    author: 'Ali Connors',
614
    ingredientsImagePath: 'food/icons/main.png',
615
    ingredientsImagePackage: _kGalleryAssetsPackage,
616 617
    description: 'With this pesto recipe, you can quickly whip up a meal to satisfy your savory needs. And if you\'re feeling festive, you can add bacon to taste.',
    imagePath: 'food/pesto_pasta.png',
618
    imagePackage: _kGalleryAssetsPackage,
619
    ingredients: <RecipeIngredient>[
620 621 622 623 624 625 626 627 628
      RecipeIngredient(amount: '1/4 cup ', description: 'Pasta'),
      RecipeIngredient(amount: '2 cups', description: 'Fresh basil leaves'),
      RecipeIngredient(amount: '1/2 cup', description: 'Parmesan cheese'),
      RecipeIngredient(amount: '1/2 cup', description: 'Extra virgin olive oil'),
      RecipeIngredient(amount: '1/3 cup', description: 'Pine nuts'),
      RecipeIngredient(amount: '1/4 cup', description: 'Lemon juice'),
      RecipeIngredient(amount: '3 cloves', description: 'Garlic'),
      RecipeIngredient(amount: '1/4 tsp', description: 'Salt'),
      RecipeIngredient(amount: '1/8 tsp', description: 'Pepper'),
629
      RecipeIngredient(amount: '3 lbs', description: 'Bacon'),
630
    ],
631
    steps: <RecipeStep>[
632
      RecipeStep(duration: '15 min', description: 'Blend'),
633
    ],
634
  ),
635
  Recipe(
636
    name: 'Cherry Pie',
637
    author: 'Sandra Adams',
638
    ingredientsImagePath: 'food/icons/main.png',
639
    ingredientsImagePackage: _kGalleryAssetsPackage,
640 641
    description: 'Sometimes when you\'re craving some cheer in your life you can jumpstart your day with some cherry pie. Dessert for breakfast is perfectly acceptable.',
    imagePath: 'food/cherry_pie.png',
642
    imagePackage: _kGalleryAssetsPackage,
643
    ingredients: <RecipeIngredient>[
644 645 646 647 648
      RecipeIngredient(amount: '1', description: 'Pie crust'),
      RecipeIngredient(amount: '4 cups', description: 'Fresh or frozen cherries'),
      RecipeIngredient(amount: '1 cup', description: 'Granulated sugar'),
      RecipeIngredient(amount: '4 tbsp', description: 'Cornstarch'),
      RecipeIngredient(amount: '1½ tbsp', description: 'Butter'),
649
    ],
650
    steps: <RecipeStep>[
651 652
      RecipeStep(duration: '15 min', description: 'Mix'),
      RecipeStep(duration: '1hr 30 min', description: 'Bake'),
653
    ],
654
  ),
655
  Recipe(
656
    name: 'Spinach Salad',
657
    author: 'Peter Carlsson',
658
    ingredientsImagePath: 'food/icons/spicy.png',
659
    ingredientsImagePackage: _kGalleryAssetsPackage,
660 661
    description: 'Everyone\'s favorite leafy green is back. Paired with fresh sliced onion, it\'s ready to tackle any dish, whether it be a salad or an egg scramble.',
    imagePath: 'food/spinach_onion_salad.png',
662
    imagePackage: _kGalleryAssetsPackage,
663
    ingredients: <RecipeIngredient>[
664 665
      RecipeIngredient(amount: '4 cups', description: 'Spinach'),
      RecipeIngredient(amount: '1 cup', description: 'Sliced onion'),
666
    ],
667
    steps: <RecipeStep>[
668
      RecipeStep(duration: '5 min', description: 'Mix'),
669
    ],
670
  ),
671
  Recipe(
672
    name: 'Butternut Squash Soup',
673
    author: 'Ali Connors',
674
    ingredientsImagePath: 'food/icons/healthy.png',
675
    ingredientsImagePackage: _kGalleryAssetsPackage,
676 677
    description: 'This creamy butternut squash soup will warm you on the chilliest of winter nights and bring a delightful pop of orange to the dinner table.',
    imagePath: 'food/butternut_squash_soup.png',
678
    imagePackage: _kGalleryAssetsPackage,
679
    ingredients: <RecipeIngredient>[
680 681 682 683 684 685 686 687
      RecipeIngredient(amount: '1', description: 'Butternut squash'),
      RecipeIngredient(amount: '4 cups', description: 'Chicken stock'),
      RecipeIngredient(amount: '2', description: 'Potatoes'),
      RecipeIngredient(amount: '1', description: 'Onion'),
      RecipeIngredient(amount: '1', description: 'Carrot'),
      RecipeIngredient(amount: '1', description: 'Celery'),
      RecipeIngredient(amount: '1 tsp', description: 'Salt'),
      RecipeIngredient(amount: '1 tsp', description: 'Pepper'),
688
    ],
689
    steps: <RecipeStep>[
690 691 692
      RecipeStep(duration: '10 min', description: 'Prep vegetables'),
      RecipeStep(duration: '5 min', description: 'Stir'),
      RecipeStep(duration: '1 hr 10 min', description: 'Cook')
693
    ],
694
  ),
695
  Recipe(
696
    name: 'Spanakopita',
697
    author: 'Trevor Hansen',
698
    ingredientsImagePath: 'food/icons/quick.png',
699
    ingredientsImagePackage: _kGalleryAssetsPackage,
700 701
    description: 'You \'feta\' believe this is a crowd-pleaser! Flaky phyllo pastry surrounds a delicious mixture of spinach and cheeses to create the perfect appetizer.',
    imagePath: 'food/spanakopita.png',
702
    imagePackage: _kGalleryAssetsPackage,
703
    ingredients: <RecipeIngredient>[
704 705 706 707 708 709
      RecipeIngredient(amount: '1 lb', description: 'Spinach'),
      RecipeIngredient(amount: '½ cup', description: 'Feta cheese'),
      RecipeIngredient(amount: '½ cup', description: 'Cottage cheese'),
      RecipeIngredient(amount: '2', description: 'Eggs'),
      RecipeIngredient(amount: '1', description: 'Onion'),
      RecipeIngredient(amount: '½ lb', description: 'Phyllo dough'),
710
    ],
711
    steps: <RecipeStep>[
712 713 714 715
      RecipeStep(duration: '5 min', description: 'Sauté vegetables'),
      RecipeStep(duration: '3 min', description: 'Stir vegetables and other filling ingredients'),
      RecipeStep(duration: '10 min', description: 'Fill phyllo squares half-full with filling and fold.'),
      RecipeStep(duration: '40 min', description: 'Bake')
716
    ],
717 718
  ),
];