// Copyright 2018 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_web/material.dart'; import 'package:flutter_web/rendering.dart'; import '../shrine_demo.dart' show ShrinePageRoute; import 'shrine_page.dart'; import 'shrine_theme.dart'; import 'shrine_types.dart'; // Displays the product title's, description, and order quantity dropdown. class _ProductItem extends StatelessWidget { const _ProductItem({ Key key, @required this.product, @required this.quantity, @required this.onChanged, }) : assert(product != null), assert(quantity != null), assert(onChanged != null), super(key: key); final Product product; final int quantity; final ValueChanged onChanged; @override Widget build(BuildContext context) { final ShrineTheme theme = ShrineTheme.of(context); return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text(product.name, style: theme.featureTitleStyle), const SizedBox(height: 24.0), Text(product.description, style: theme.featureStyle), const SizedBox(height: 16.0), Padding( padding: const EdgeInsets.only(top: 8.0, bottom: 8.0, right: 88.0), child: DropdownButtonHideUnderline( child: Container( decoration: BoxDecoration( border: Border.all( color: const Color(0xFFD9D9D9), ), ), child: DropdownButton( items: [0, 1, 2, 3, 4, 5] .map>((int value) { return DropdownMenuItem( value: value, child: Padding( padding: const EdgeInsets.only(left: 8.0), child: Text('Quantity $value', style: theme.quantityMenuStyle), ), ); }).toList(), value: quantity, onChanged: onChanged, ), ), ), ), ], ); } } // Vendor name and description class _VendorItem extends StatelessWidget { const _VendorItem({Key key, @required this.vendor}) : assert(vendor != null), super(key: key); final Vendor vendor; @override Widget build(BuildContext context) { final ShrineTheme theme = ShrineTheme.of(context); return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SizedBox( height: 24.0, child: Align( alignment: Alignment.bottomLeft, child: Text(vendor.name, style: theme.vendorTitleStyle), ), ), const SizedBox(height: 16.0), Text(vendor.description, style: theme.vendorStyle), ], ); } } // Layout the order page's heading: the product's image, the // title/description/dropdown product item, and the vendor item. class _HeadingLayout extends MultiChildLayoutDelegate { _HeadingLayout(); static const String image = 'image'; static const String icon = 'icon'; static const String product = 'product'; static const String vendor = 'vendor'; @override void performLayout(Size size) { const double margin = 56.0; final bool landscape = size.width > size.height; final double imageWidth = (landscape ? size.width / 2.0 : size.width) - margin * 2.0; final BoxConstraints imageConstraints = BoxConstraints(maxHeight: 224.0, maxWidth: imageWidth); final Size imageSize = layoutChild(image, imageConstraints); const double imageY = 0.0; positionChild(image, const Offset(margin, imageY)); final double productWidth = landscape ? size.width / 2.0 : size.width - margin; final BoxConstraints productConstraints = BoxConstraints(maxWidth: productWidth); final Size productSize = layoutChild(product, productConstraints); final double productX = landscape ? size.width / 2.0 : margin; final double productY = landscape ? 0.0 : imageY + imageSize.height + 16.0; positionChild(product, Offset(productX, productY)); final Size iconSize = layoutChild(icon, BoxConstraints.loose(size)); positionChild( icon, Offset(productX - iconSize.width - 16.0, productY + 8.0)); final double vendorWidth = landscape ? size.width - margin : productWidth; layoutChild(vendor, BoxConstraints(maxWidth: vendorWidth)); final double vendorX = landscape ? margin : productX; final double vendorY = productY + productSize.height + 16.0; positionChild(vendor, Offset(vendorX, vendorY)); } @override bool shouldRelayout(_HeadingLayout oldDelegate) => true; } // Describes a product and vendor in detail, supports specifying // a order quantity (0-5). Appears at the top of the OrderPage. class _Heading extends StatelessWidget { const _Heading({ Key key, @required this.product, @required this.quantity, this.quantityChanged, }) : assert(product != null), assert(quantity != null && quantity >= 0 && quantity <= 5), super(key: key); final Product product; final int quantity; final ValueChanged quantityChanged; @override Widget build(BuildContext context) { final Size screenSize = MediaQuery.of(context).size; return SizedBox( height: (screenSize.height - kToolbarHeight) * 1.35, child: Material( type: MaterialType.card, elevation: 0.0, child: Padding( padding: const EdgeInsets.only( left: 16.0, top: 18.0, right: 16.0, bottom: 24.0), child: CustomMultiChildLayout( delegate: _HeadingLayout(), children: [ LayoutId( id: _HeadingLayout.image, child: Hero( tag: product.tag, child: Image.asset( product.imageAsset, package: product.imageAssetPackage, fit: BoxFit.contain, alignment: Alignment.center, ), ), ), LayoutId( id: _HeadingLayout.icon, child: const Icon( Icons.info_outline, size: 24.0, color: Color(0xFFFFE0E0), ), ), LayoutId( id: _HeadingLayout.product, child: _ProductItem( product: product, quantity: quantity, onChanged: quantityChanged, ), ), LayoutId( id: _HeadingLayout.vendor, child: _VendorItem(vendor: product.vendor), ), ], ), ), ), ); } } class OrderPage extends StatefulWidget { OrderPage({ Key key, @required this.order, @required this.products, @required this.shoppingCart, }) : assert(order != null), assert(products != null && products.isNotEmpty), assert(shoppingCart != null), super(key: key); final Order order; final List products; final Map shoppingCart; @override _OrderPageState createState() => _OrderPageState(); } // Displays a product's heading above photos of all of the other products // arranged in two columns. Enables the user to specify a quantity and add an // order to the shopping cart. class _OrderPageState extends State { GlobalKey scaffoldKey; @override void initState() { super.initState(); scaffoldKey = GlobalKey(debugLabel: 'Shrine Order ${widget.order}'); } Order get currentOrder => ShrineOrderRoute.of(context).order; set currentOrder(Order value) { ShrineOrderRoute.of(context).order = value; } void updateOrder({int quantity, bool inCart}) { final Order newOrder = currentOrder.copyWith(quantity: quantity, inCart: inCart); if (currentOrder != newOrder) { setState(() { widget.shoppingCart[newOrder.product] = newOrder; currentOrder = newOrder; }); } } void showSnackBarMessage(String message) { scaffoldKey.currentState.showSnackBar(SnackBar(content: Text(message))); } @override Widget build(BuildContext context) { return ShrinePage( scaffoldKey: scaffoldKey, products: widget.products, shoppingCart: widget.shoppingCart, floatingActionButton: FloatingActionButton( onPressed: () { updateOrder(inCart: true); final int n = currentOrder.quantity; final String item = currentOrder.product.name; showSnackBarMessage( 'There ${n == 1 ? "is one $item item" : "are $n $item items"} in the shopping cart.'); }, backgroundColor: const Color(0xFF16F0F0), tooltip: 'Add to cart', child: const Icon( Icons.add_shopping_cart, color: Colors.black, ), ), body: CustomScrollView( slivers: [ SliverToBoxAdapter( child: _Heading( product: widget.order.product, quantity: currentOrder.quantity, quantityChanged: (int value) { updateOrder(quantity: value); }, ), ), SliverSafeArea( top: false, minimum: const EdgeInsets.fromLTRB(8.0, 32.0, 8.0, 8.0), sliver: SliverGrid( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 248.0, mainAxisSpacing: 8.0, crossAxisSpacing: 8.0, ), delegate: SliverChildListDelegate( widget.products .where((Product product) => product != widget.order.product) .map((Product product) { return Card( elevation: 1.0, child: Image.asset( product.imageAsset, package: product.imageAssetPackage, fit: BoxFit.contain, ), ); }).toList(), ), ), ), ], ), ); } } // Displays a full-screen modal OrderPage. // // The order field will be replaced each time the user reconfigures the order. // When the user backs out of this route the completer's value will be the // final value of the order field. class ShrineOrderRoute extends ShrinePageRoute { ShrineOrderRoute({ @required this.order, WidgetBuilder builder, RouteSettings settings, }) : assert(order != null), super(builder: builder, settings: settings); Order order; @override Order get currentResult => order; static ShrineOrderRoute of(BuildContext context) => ModalRoute.of(context); }