From ee22dc98e6f0349a7e2120c96ae2e9e858622ce3 Mon Sep 17 00:00:00 2001 From: mike doornenbal Date: Thu, 4 Jul 2024 15:01:28 +0200 Subject: [PATCH] feat: add interface to flutter_product_page --- .../lib/flutter_product_page.dart | 8 +- ...t_page_category_styling_configuration.dart | 54 ---- .../product_page_configuration.dart | 229 +++++++--------- .../configuration/product_page_content.dart | 16 -- ...on.dart => product_page_translations.dart} | 4 +- .../lib/src/models/product_page_shop.dart | 18 -- .../lib/src/product_page_screen.dart | 238 +++++++++++++++++ .../lib/src/services/category_service.dart | 29 +- .../src/services/selected_shop_service.dart | 21 -- .../src/services/shopping_cart_notifier.dart | 10 - .../lib/src/ui/product_page.dart | 252 ------------------ .../lib/src/ui/product_page_screen.dart | 36 --- .../widgets/horizontal_list_items.dart | 6 +- .../components => widgets}/product_item.dart | 21 +- .../{ui => }/widgets/product_item_popup.dart | 8 +- .../components => widgets}/shop_selector.dart | 17 +- .../lib/src/{ui => }/widgets/spaced_wrap.dart | 6 +- .../weekly_discount.dart | 9 +- packages/flutter_product_page/pubspec.yaml | 14 +- .../lib/src/config/shopping_cart_config.dart | 191 +++++++------ .../lib/src/widgets/shopping_cart_screen.dart | 103 ++++--- 21 files changed, 550 insertions(+), 740 deletions(-) delete mode 100644 packages/flutter_product_page/lib/src/configuration/product_page_category_styling_configuration.dart delete mode 100644 packages/flutter_product_page/lib/src/configuration/product_page_content.dart rename packages/flutter_product_page/lib/src/configuration/{product_page_localization.dart => product_page_translations.dart} (89%) delete mode 100644 packages/flutter_product_page/lib/src/models/product_page_shop.dart create mode 100644 packages/flutter_product_page/lib/src/product_page_screen.dart delete mode 100644 packages/flutter_product_page/lib/src/services/selected_shop_service.dart delete mode 100644 packages/flutter_product_page/lib/src/services/shopping_cart_notifier.dart delete mode 100644 packages/flutter_product_page/lib/src/ui/product_page.dart delete mode 100644 packages/flutter_product_page/lib/src/ui/product_page_screen.dart rename packages/flutter_product_page/lib/src/{ui => }/widgets/horizontal_list_items.dart (93%) rename packages/flutter_product_page/lib/src/{ui/components => widgets}/product_item.dart (89%) rename packages/flutter_product_page/lib/src/{ui => }/widgets/product_item_popup.dart (90%) rename packages/flutter_product_page/lib/src/{ui/components => widgets}/shop_selector.dart (73%) rename packages/flutter_product_page/lib/src/{ui => }/widgets/spaced_wrap.dart (93%) rename packages/flutter_product_page/lib/src/{ui/components => widgets}/weekly_discount.dart (90%) diff --git a/packages/flutter_product_page/lib/flutter_product_page.dart b/packages/flutter_product_page/lib/flutter_product_page.dart index dbcd53a..f15fb25 100644 --- a/packages/flutter_product_page/lib/flutter_product_page.dart +++ b/packages/flutter_product_page/lib/flutter_product_page.dart @@ -2,11 +2,7 @@ /// detailed view of each product. library flutter_product_page; -export "src/configuration/product_page_category_styling_configuration.dart"; export "src/configuration/product_page_configuration.dart"; -export "src/configuration/product_page_content.dart"; -export "src/configuration/product_page_localization.dart"; export "src/configuration/product_page_shop_selector_style.dart"; -export "src/models/product_page_shop.dart"; -export "src/ui/product_page.dart"; -export "src/ui/product_page_screen.dart"; +export "src/configuration/product_page_translations.dart"; +export "src/product_page_screen.dart"; diff --git a/packages/flutter_product_page/lib/src/configuration/product_page_category_styling_configuration.dart b/packages/flutter_product_page/lib/src/configuration/product_page_category_styling_configuration.dart deleted file mode 100644 index f554222..0000000 --- a/packages/flutter_product_page/lib/src/configuration/product_page_category_styling_configuration.dart +++ /dev/null @@ -1,54 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_nested_categories/flutter_nested_categories.dart" - show CategoryHeaderStyling; - -/// Configuration for the styling of the category list on the product page. -/// This configuration allows to customize the title, header styling and -/// the collapsible behavior of the categories. -class ProductPageCategoryStylingConfiguration { - /// Constructor to create a new instance of - /// [ProductPageCategoryStylingConfiguration]. - const ProductPageCategoryStylingConfiguration({ - this.headerStyling, - this.headerCentered = false, - this.customTitle, - this.title, - this.titleStyle, - this.titleCentered = false, - this.isCategoryCollapsible = true, - }); - - /// Optional title for the category list. This will be displayed at the - /// top of the list. - final String? title; - - /// Optional custom title widget for the category list. This will be - /// displayed at the top of the list. If set, the text title will be - /// ignored. - final Widget? customTitle; - - /// Optional title style for the title of the category list. This will - /// be applied to the title of the category list. If not set, the default - /// text style will be used. - final TextStyle? titleStyle; - - /// Configure if the title should be centered. - /// - /// Default is false. - final bool titleCentered; - - /// Optional header styling for the categories. This will be applied to - /// the name of the categories. If not set, the default text style will - /// be used. - final CategoryHeaderStyling? headerStyling; - - /// Configure if the category header should be centered. - /// - /// Default is false. - final bool headerCentered; - - /// Configure if the category should be collapsible. - /// - /// Default is true. - final bool isCategoryCollapsible; -} diff --git a/packages/flutter_product_page/lib/src/configuration/product_page_configuration.dart b/packages/flutter_product_page/lib/src/configuration/product_page_configuration.dart index d3f440c..edb3527 100644 --- a/packages/flutter_product_page/lib/src/configuration/product_page_configuration.dart +++ b/packages/flutter_product_page/lib/src/configuration/product_page_configuration.dart @@ -1,109 +1,48 @@ import "package:flutter/material.dart"; -import "package:flutter_product_page/src/services/shopping_cart_notifier.dart"; -import "package:flutter_product_page/src/ui/widgets/product_item_popup.dart"; -import "package:flutter_shopping/flutter_shopping.dart"; +import "package:flutter_product_page/flutter_product_page.dart"; +import "package:flutter_product_page/src/widgets/product_item_popup.dart"; +import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; /// Configuration for the product page. class ProductPageConfiguration { /// Constructor for the product page configuration. ProductPageConfiguration({ + required this.shoppingService, required this.shops, required this.getProducts, required this.onAddToCart, required this.onNavigateToShoppingCart, - this.navigateToShoppingCartBuilder = _defaultNavigateToShoppingCartBuilder, + required this.getProductsInShoppingCart, + this.shoppingCartButtonBuilder = _defaultShoppingCartButtonBuilder, this.initialShopId, this.productBuilder, this.onShopSelectionChange, - this.getProductsInShoppingCart, - this.localizations = const ProductPageLocalization(), + this.translations = const ProductPageTranslations(), this.shopSelectorStyle = ShopSelectorStyle.spacedWrap, - this.categoryStylingConfiguration = - const ProductPageCategoryStylingConfiguration(), this.pagePadding = const EdgeInsets.all(4), this.appBar = _defaultAppBar, this.bottomNavigationBar, - Function( - BuildContext context, - Product product, - )? onProductDetail, - String Function( - Product product, - )? getDiscountDescription, - Widget Function( - BuildContext context, - Product product, - )? productPopupBuilder, - Widget Function( - BuildContext context, - )? noContentBuilder, - Widget Function( - BuildContext context, - Object? error, - StackTrace? stackTrace, - )? errorBuilder, - }) { - _productPopupBuilder = productPopupBuilder; - _productPopupBuilder ??= - (BuildContext context, Product product) => ProductItemPopup( - product: product, - configuration: this, - ); + this.onProductDetail = _onProductDetail, + this.discountDescription = _defaultDiscountDescription, + this.noContentBuilder = _defaultNoContentBuilder, + this.errorBuilder = _defaultErrorBuilder, + }); - _onProductDetail = onProductDetail; - _onProductDetail ??= (BuildContext context, Product product) async { - var theme = Theme.of(context); - - await showModalBottomSheet( - context: context, - backgroundColor: theme.colorScheme.surface, - builder: (context) => _productPopupBuilder!( - context, - product, - ), - ); - }; - - _noContentBuilder = noContentBuilder; - _noContentBuilder ??= (BuildContext context) { - var theme = Theme.of(context); - return Center( - child: Text( - "No content", - style: theme.textTheme.titleLarge, - ), - ); - }; - - _errorBuilder = errorBuilder; - _errorBuilder ??= - (BuildContext context, Object? error, StackTrace? stackTrace) { - var theme = Theme.of(context); - return Center( - child: Text( - "Error: $error", - style: theme.textTheme.titleLarge, - ), - ); - }; - - _getDiscountDescription = getDiscountDescription; - _getDiscountDescription ??= (Product product) => - "${product.name}, now for ${product.discountPrice} each"; - } + /// The shopping service that is used + final ShoppingService shoppingService; /// The shop that is initially selected. final String? initialShopId; /// A list of all the shops that the user must be able to navigate from. - final Future> shops; + final Future> Function() shops; /// A function that returns all the products that belong to a certain shop. - /// The function must return a [ProductPageContent] object. - final Future Function(ProductPageShop shop) getProducts; + /// The function must return a [List]. + final Future> Function(Shop shop) getProducts; /// The localizations for the product page. - final ProductPageLocalization localizations; + final ProductPageTranslations translations; /// Builder for the product item. These items will be displayed in the list /// for each product in their seperated category. This builder should only @@ -111,55 +50,24 @@ class ProductPageConfiguration { /// in-case the developer does not override it. Widget Function(BuildContext context, Product product)? productBuilder; - late Widget Function(BuildContext context, Product product)? - _productPopupBuilder; - - /// The builder for the product popup. This popup will be displayed when the - /// user clicks on a product. This builder should only build the widget that - /// displays the content of one specific product. - /// This builder has a default in-case the developer - Widget Function(BuildContext context, Product product) - get productPopupBuilder => _productPopupBuilder!; - - late Function(BuildContext context, Product product)? _onProductDetail; - - /// This function handles the creation of the product detail popup. This - /// function has a default in-case the developer does not override it. - /// The default intraction is a popup, but this can be overriden. - Function(BuildContext context, Product product) get onProductDetail => - _onProductDetail!; - - late Widget Function(BuildContext context)? _noContentBuilder; - - /// The no content builder is used when a shop has no products. This builder - /// has a default in-case the developer does not override it. - Function(BuildContext context)? get noContentBuilder => _noContentBuilder; + /// The builder for the product popup. This builder should return a widget + Function( + BuildContext context, + Product product, + String closeText, + ) onProductDetail; /// The builder for the shopping cart. This builder should return a widget /// that navigates to the shopping cart overview page. Widget Function( BuildContext context, ProductPageConfiguration configuration, - ShoppingCartNotifier notifier, - ) navigateToShoppingCartBuilder; + ) shoppingCartButtonBuilder; - late Widget Function( - BuildContext context, - Object? error, - StackTrace? stackTrace, - )? _errorBuilder; - - /// The error builder is used when an error occurs. This builder has a default - /// in-case the developer does not override it. - Widget Function(BuildContext context, Object? error, StackTrace? stackTrace)? - get errorBuilder => _errorBuilder; - - late String Function(Product product)? _getDiscountDescription; - - /// The function that returns the description of the discount for a product. - /// This allows you to translate and give custom messages for each product. - String Function(Product product)? get getDiscountDescription => - _getDiscountDescription!; + /// The function that returns the discount description for a product. + String Function( + Product product, + ) discountDescription; /// This function must be implemented by the developer and should handle the /// adding of a product to the cart. @@ -167,11 +75,11 @@ class ProductPageConfiguration { /// This function gets executed when the user changes the shop selection. /// This function always fires upon first load with the initial shop as well. - final Function(ProductPageShop shop)? onShopSelectionChange; + final Function(Shop shop)? onShopSelectionChange; /// This function must be implemented by the developer and should handle the /// navigation to the shopping cart overview page. - final int Function()? getProductsInShoppingCart; + final int Function() getProductsInShoppingCart; /// This function must be implemented by the developer and should handle the /// navigation to the shopping cart overview page. @@ -180,9 +88,6 @@ class ProductPageConfiguration { /// The style of the shop selector. final ShopSelectorStyle shopSelectorStyle; - /// The styling configuration for the category list. - final ProductPageCategoryStylingConfiguration categoryStylingConfiguration; - /// The padding for the page. final EdgeInsets pagePadding; @@ -191,6 +96,20 @@ class ProductPageConfiguration { /// Optional app bar that you can pass to the order detail screen. final AppBar Function(BuildContext context)? appBar; + + /// Builder for the no content widget. This builder is used when there is no + /// content to display. + final Widget Function( + BuildContext context, + )? noContentBuilder; + + /// Builder for the error widget. This builder is used when there is an error + /// to display. + Widget Function( + BuildContext context, + Object? error, + StackTrace? stackTrace, + ) errorBuilder; } AppBar _defaultAppBar( @@ -210,21 +129,21 @@ AppBar _defaultAppBar( ); } -Widget _defaultNavigateToShoppingCartBuilder( +Widget _defaultShoppingCartButtonBuilder( BuildContext context, ProductPageConfiguration configuration, - ShoppingCartNotifier notifier, ) { var theme = Theme.of(context); return ListenableBuilder( - listenable: notifier, + listenable: configuration.shoppingService.shoppingCartService, builder: (context, widget) => Padding( padding: const EdgeInsets.symmetric(horizontal: 60), child: SizedBox( width: double.infinity, child: FilledButton( - onPressed: configuration.getProductsInShoppingCart?.call() != 0 + onPressed: configuration + .shoppingService.shoppingCartService.products.isNotEmpty ? configuration.onNavigateToShoppingCart : null, style: theme.filledButtonTheme.style?.copyWith( @@ -238,7 +157,7 @@ Widget _defaultNavigateToShoppingCartBuilder( vertical: 12, ), child: Text( - configuration.localizations.navigateToShoppingCart, + configuration.translations.navigateToShoppingCart, style: theme.textTheme.displayLarge, ), ), @@ -247,3 +166,51 @@ Widget _defaultNavigateToShoppingCartBuilder( ), ); } + +Future _onProductDetail( + BuildContext context, + Product product, + String closeText, +) async { + var theme = Theme.of(context); + + await showModalBottomSheet( + context: context, + backgroundColor: theme.colorScheme.surface, + builder: (context) => ProductItemPopup( + product: product, + closeText: closeText, + ), + ); +} + +String _defaultDiscountDescription( + Product product, +) => + "${product.name}, now for ${product.discountPrice} each"; + +Widget _defaultNoContentBuilder( + BuildContext context, +) { + var theme = Theme.of(context); + return Center( + child: Text( + "No content", + style: theme.textTheme.titleLarge, + ), + ); +} + +Widget _defaultErrorBuilder( + BuildContext context, + Object? error, + StackTrace? stackTrace, +) { + var theme = Theme.of(context); + return Center( + child: Text( + "Error: $error", + style: theme.textTheme.titleLarge, + ), + ); +} diff --git a/packages/flutter_product_page/lib/src/configuration/product_page_content.dart b/packages/flutter_product_page/lib/src/configuration/product_page_content.dart deleted file mode 100644 index 018d822..0000000 --- a/packages/flutter_product_page/lib/src/configuration/product_page_content.dart +++ /dev/null @@ -1,16 +0,0 @@ -import "package:flutter_shopping/flutter_shopping.dart"; - -/// Return type that contains the products and an optional discounted product. -class ProductPageContent { - /// Default constructor for this class. - const ProductPageContent({ - required this.products, - this.discountedProduct, - }); - - /// List of products that belong to the shop. - final List products; - - /// Optional highlighted discounted product to display. - final Product? discountedProduct; -} diff --git a/packages/flutter_product_page/lib/src/configuration/product_page_localization.dart b/packages/flutter_product_page/lib/src/configuration/product_page_translations.dart similarity index 89% rename from packages/flutter_product_page/lib/src/configuration/product_page_localization.dart rename to packages/flutter_product_page/lib/src/configuration/product_page_translations.dart index f2bbe3f..0129a53 100644 --- a/packages/flutter_product_page/lib/src/configuration/product_page_localization.dart +++ b/packages/flutter_product_page/lib/src/configuration/product_page_translations.dart @@ -1,7 +1,7 @@ /// Localization for the product page -class ProductPageLocalization { +class ProductPageTranslations { /// Default constructor - const ProductPageLocalization({ + const ProductPageTranslations({ this.navigateToShoppingCart = "View shopping cart", this.discountTitle = "Weekly offer", this.failedToLoadImageExplenation = "Failed to load image", diff --git a/packages/flutter_product_page/lib/src/models/product_page_shop.dart b/packages/flutter_product_page/lib/src/models/product_page_shop.dart deleted file mode 100644 index a4d0aa9..0000000 --- a/packages/flutter_product_page/lib/src/models/product_page_shop.dart +++ /dev/null @@ -1,18 +0,0 @@ -/// The product page shop class contains all the required information -/// that needs to be known about a certain shop. -/// -/// In your own implemententation, you must extend from this class so you can -/// add more fields to this class to suit your needs. -class ProductPageShop { - /// The default constructor for this class. - const ProductPageShop({ - required this.id, - required this.name, - }); - - /// The unique identifier for the shop. - final String id; - - /// The name of the shop. - final String name; -} diff --git a/packages/flutter_product_page/lib/src/product_page_screen.dart b/packages/flutter_product_page/lib/src/product_page_screen.dart new file mode 100644 index 0000000..85aaf6f --- /dev/null +++ b/packages/flutter_product_page/lib/src/product_page_screen.dart @@ -0,0 +1,238 @@ +import "package:flutter/material.dart"; +import "package:flutter_product_page/flutter_product_page.dart"; +import "package:flutter_product_page/src/services/category_service.dart"; +import "package:flutter_product_page/src/widgets/shop_selector.dart"; +import "package:flutter_product_page/src/widgets/weekly_discount.dart"; +import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; + +/// A page that displays products. +class ProductPageScreen extends StatelessWidget { + /// Constructor for the product page. + const ProductPageScreen({ + required this.configuration, + this.initialBuildShopId, + super.key, + }); + + /// Configuration for the product page. + final ProductPageConfiguration configuration; + + /// An optional initial shop ID to select. This overrides the initialShopId + /// from the configuration. + final String? initialBuildShopId; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: configuration.appBar!.call(context), + bottomNavigationBar: configuration.bottomNavigationBar, + body: SafeArea( + child: Padding( + padding: configuration.pagePadding, + child: FutureBuilder( + // ignore: discarded_futures + future: configuration.shops(), + builder: (BuildContext context, AsyncSnapshot data) { + if (data.connectionState == ConnectionState.waiting) { + return const Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center(child: CircularProgressIndicator.adaptive()), + ], + ); + } + + if (data.hasError) { + return configuration.errorBuilder( + context, + data.error, + data.stackTrace, + ); + } + + List? shops = data.data; + + if (shops == null || shops.isEmpty) { + return configuration.errorBuilder(context, null, null); + } + + if (initialBuildShopId != null) { + Shop? initialShop; + + for (var shop in shops) { + if (shop.id == initialBuildShopId) { + initialShop = shop; + break; + } + } + + configuration.shoppingService.shopService + .selectShop(initialShop ?? shops.first); + } else if (configuration.initialShopId != null) { + Shop? initialShop; + + for (var shop in shops) { + if (shop.id == configuration.initialShopId) { + initialShop = shop; + break; + } + } + + configuration.shoppingService.shopService + .selectShop(initialShop ?? shops.first); + } else { + configuration.shoppingService.shopService + .selectShop(shops.first); + } + + return ListenableBuilder( + listenable: configuration.shoppingService.shopService, + builder: (BuildContext context, Widget? _) { + configuration.onShopSelectionChange?.call( + configuration.shoppingService.shopService.selectedShop!, + ); + return _ProductPage( + configuration: configuration, + shops: shops, + ); + }, + ); + }, + ), + ), + ), + ); +} + +class _ProductPage extends StatelessWidget { + const _ProductPage({ + required this.configuration, + required this.shops, + }); + + final ProductPageConfiguration configuration; + + final List shops; + + @override + Widget build(BuildContext context) { + var pageContent = SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ShopSelector( + configuration: configuration, + shops: shops, + onTap: configuration.shoppingService.shopService.selectShop, + ), + _ShopContents( + configuration: configuration, + ), + ], + ), + ); + + return Stack( + children: [ + pageContent, + Align( + alignment: Alignment.bottomCenter, + child: configuration.shoppingCartButtonBuilder( + context, + configuration, + ), + ), + ], + ); + } +} + +class _ShopContents extends StatelessWidget { + const _ShopContents({ + required this.configuration, + }); + + final ProductPageConfiguration configuration; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + return Padding( + padding: EdgeInsets.symmetric( + horizontal: configuration.pagePadding.horizontal, + ), + child: FutureBuilder( + // ignore: discarded_futures + future: configuration.getProducts( + configuration.shoppingService.shopService.selectedShop!, + ), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator.adaptive()); + } + + if (snapshot.hasError) { + return configuration.errorBuilder( + context, + snapshot.error, + snapshot.stackTrace, + ); + } + + var productPageContent = snapshot.data; + + if (productPageContent == null || productPageContent.isEmpty) { + return configuration.noContentBuilder!(context); + } + + var productList = Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), + child: Column( + children: [ + // Products + getCategoryList( + context, + configuration, + productPageContent, + ), + + // Bottom padding so the last product is not cut off + // by the to shopping cart button. + const SizedBox(height: 48), + ], + ), + ); + var discountedproducts = productPageContent + .where((product) => product.hasDiscount) + .toList(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Discounted product + if (discountedproducts.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: WeeklyDiscount( + configuration: configuration, + product: discountedproducts.first, + ), + ), + ], + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 24), + child: Text( + "What would you like to order?", + style: theme.textTheme.titleLarge, + textAlign: TextAlign.start, + ), + ), + + productList, + ], + ); + }, + ), + ); + } +} diff --git a/packages/flutter_product_page/lib/src/services/category_service.dart b/packages/flutter_product_page/lib/src/services/category_service.dart index 5d39843..c09def2 100644 --- a/packages/flutter_product_page/lib/src/services/category_service.dart +++ b/packages/flutter_product_page/lib/src/services/category_service.dart @@ -1,28 +1,14 @@ import "package:flutter/material.dart"; import "package:flutter_nested_categories/flutter_nested_categories.dart"; -import "package:flutter_product_page/src/services/shopping_cart_notifier.dart"; -import "package:flutter_product_page/src/ui/components/product_item.dart"; -import "package:flutter_shopping/flutter_shopping.dart"; - -/// A function that is called when a product is added to the cart. -Product onAddToCartWrapper( - ProductPageConfiguration configuration, - ShoppingCartNotifier shoppingCartNotifier, - Product product, -) { - shoppingCartNotifier.productsChanged(); - - configuration.onAddToCart(product); - - return product; -} +import "package:flutter_product_page/flutter_product_page.dart"; +import "package:flutter_product_page/src/widgets/product_item.dart"; +import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; /// Generates a [CategoryList] from a list of [Product]s and a /// [ProductPageConfiguration]. Widget getCategoryList( BuildContext context, ProductPageConfiguration configuration, - ShoppingCartNotifier shoppingCartNotifier, List products, ) { var theme = Theme.of(context); @@ -44,12 +30,9 @@ Widget getCategoryList( : ProductItem( product: product, onProductDetail: configuration.onProductDetail, - onAddToCart: (Product product) => onAddToCartWrapper( - configuration, - shoppingCartNotifier, - product, - ), - localizations: configuration.localizations, + onAddToCart: (Product product) => + configuration.onAddToCart(product), + translations: configuration.translations, ), ) .toList(); diff --git a/packages/flutter_product_page/lib/src/services/selected_shop_service.dart b/packages/flutter_product_page/lib/src/services/selected_shop_service.dart deleted file mode 100644 index 502eaa5..0000000 --- a/packages/flutter_product_page/lib/src/services/selected_shop_service.dart +++ /dev/null @@ -1,21 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_product_page/src/models/product_page_shop.dart"; - -/// A service that provides the currently selected shop. -class SelectedShopService extends ChangeNotifier { - /// Creates a [SelectedShopService]. - SelectedShopService(); - - ProductPageShop? _selectedShop; - - /// Updates the selected shop. - void selectShop(ProductPageShop shop) { - if (_selectedShop == shop) return; - - _selectedShop = shop; - notifyListeners(); - } - - /// The currently selected shop. - ProductPageShop? get selectedShop => _selectedShop; -} diff --git a/packages/flutter_product_page/lib/src/services/shopping_cart_notifier.dart b/packages/flutter_product_page/lib/src/services/shopping_cart_notifier.dart deleted file mode 100644 index d02251f..0000000 --- a/packages/flutter_product_page/lib/src/services/shopping_cart_notifier.dart +++ /dev/null @@ -1,10 +0,0 @@ -import "package:flutter/material.dart"; - -/// Class that notifies listeners when the products in the shopping cart have -/// changed. -class ShoppingCartNotifier extends ChangeNotifier { - /// Notifies listeners that the products in the shopping cart have changed. - void productsChanged() { - notifyListeners(); - } -} diff --git a/packages/flutter_product_page/lib/src/ui/product_page.dart b/packages/flutter_product_page/lib/src/ui/product_page.dart deleted file mode 100644 index bbdfe8f..0000000 --- a/packages/flutter_product_page/lib/src/ui/product_page.dart +++ /dev/null @@ -1,252 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_product_page/flutter_product_page.dart"; -import "package:flutter_product_page/src/services/category_service.dart"; -import "package:flutter_product_page/src/services/selected_shop_service.dart"; -import "package:flutter_product_page/src/services/shopping_cart_notifier.dart"; -import "package:flutter_product_page/src/ui/components/shop_selector.dart"; -import "package:flutter_product_page/src/ui/components/weekly_discount.dart"; - -/// A page that displays products. -class ProductPage extends StatelessWidget { - /// Constructor for the product page. - ProductPage({ - required this.configuration, - this.initialBuildShopId, - super.key, - }); - - /// Configuration for the product page. - final ProductPageConfiguration configuration; - - /// An optional initial shop ID to select. This overrides the initialShopId - /// from the configuration. - final String? initialBuildShopId; - - late final SelectedShopService _selectedShopService = SelectedShopService(); - - late final ShoppingCartNotifier _shoppingCartNotifier = - ShoppingCartNotifier(); - - @override - Widget build(BuildContext context) => Padding( - padding: configuration.pagePadding, - child: FutureBuilder( - future: configuration.shops, - builder: (BuildContext context, AsyncSnapshot data) { - if (data.connectionState == ConnectionState.waiting) { - return const Align( - alignment: Alignment.center, - child: CircularProgressIndicator.adaptive(), - ); - } - - if (data.hasError) { - return configuration.errorBuilder!( - context, - data.error, - data.stackTrace, - ); - } - - List? shops = data.data; - - if (shops == null || shops.isEmpty) { - return configuration.errorBuilder!(context, null, null); - } - - if (initialBuildShopId != null) { - ProductPageShop? initialShop; - - for (var shop in shops) { - if (shop.id == initialBuildShopId) { - initialShop = shop; - break; - } - } - - _selectedShopService.selectShop(initialShop ?? shops.first); - } else if (configuration.initialShopId != null) { - ProductPageShop? initialShop; - - for (var shop in shops) { - if (shop.id == configuration.initialShopId) { - initialShop = shop; - break; - } - } - - _selectedShopService.selectShop(initialShop ?? shops.first); - } else { - _selectedShopService.selectShop(shops.first); - } - - return ListenableBuilder( - listenable: _selectedShopService, - builder: (BuildContext context, Widget? _) { - configuration.onShopSelectionChange?.call( - _selectedShopService.selectedShop!, - ); - return _ProductPage( - configuration: configuration, - selectedShopService: _selectedShopService, - shoppingCartNotifier: _shoppingCartNotifier, - shops: shops, - ); - }, - ); - }, - ), - ); -} - -class _ProductPage extends StatelessWidget { - const _ProductPage({ - required this.configuration, - required this.selectedShopService, - required this.shoppingCartNotifier, - required this.shops, - }); - - final ProductPageConfiguration configuration; - final SelectedShopService selectedShopService; - final ShoppingCartNotifier shoppingCartNotifier; - - final List shops; - - void _onTapChangeShop(ProductPageShop shop) { - selectedShopService.selectShop(shop); - } - - @override - Widget build(BuildContext context) { - var pageContent = SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ShopSelector( - configuration: configuration, - selectedShopService: selectedShopService, - shops: shops, - onTap: _onTapChangeShop, - ), - _ShopContents( - configuration: configuration, - selectedShopService: selectedShopService, - shoppingCartNotifier: shoppingCartNotifier, - ), - ], - ), - ); - - return Stack( - children: [ - pageContent, - Align( - alignment: Alignment.bottomCenter, - child: configuration.navigateToShoppingCartBuilder( - context, - configuration, - shoppingCartNotifier, - ), - ), - ], - ); - } -} - -class _ShopContents extends StatelessWidget { - const _ShopContents({ - required this.configuration, - required this.selectedShopService, - required this.shoppingCartNotifier, - }); - - final ProductPageConfiguration configuration; - final SelectedShopService selectedShopService; - final ShoppingCartNotifier shoppingCartNotifier; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - return Padding( - padding: EdgeInsets.symmetric( - horizontal: configuration.pagePadding.horizontal, - ), - child: FutureBuilder( - // ignore: discarded_futures - future: configuration.getProducts( - selectedShopService.selectedShop!, - ), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Align( - alignment: Alignment.center, - child: CircularProgressIndicator.adaptive(), - ); - } - - if (snapshot.hasError) { - return configuration.errorBuilder!( - context, - snapshot.error, - snapshot.stackTrace, - ); - } - - var productPageContent = snapshot.data; - - if (productPageContent == null || - productPageContent.products.isEmpty) { - return configuration.noContentBuilder!(context); - } - - var productList = Padding( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), - child: Column( - children: [ - // Products - getCategoryList( - context, - configuration, - shoppingCartNotifier, - productPageContent.products, - ), - - // Bottom padding so the last product is not cut off - // by the to shopping cart button. - const SizedBox(height: 48), - ], - ), - ); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Discounted product - if (productPageContent.discountedProduct != null) ...[ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: WeeklyDiscount( - configuration: configuration, - product: productPageContent.discountedProduct!, - ), - ), - ], - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 24), - child: Text( - "What would you like to order?", - style: theme.textTheme.titleLarge, - textAlign: TextAlign.start, - ), - ), - - productList, - ], - ); - }, - ), - ); - } -} diff --git a/packages/flutter_product_page/lib/src/ui/product_page_screen.dart b/packages/flutter_product_page/lib/src/ui/product_page_screen.dart deleted file mode 100644 index ceaa158..0000000 --- a/packages/flutter_product_page/lib/src/ui/product_page_screen.dart +++ /dev/null @@ -1,36 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_product_page/src/configuration/product_page_configuration.dart"; -import "package:flutter_product_page/src/ui/product_page.dart"; - -/// A screen that displays a product page. This screen contains a Scaffold, -/// in which the body is a SafeArea that contains a ProductPage widget. -/// -/// If you do not wish to create a Scaffold you can use the -/// [ProductPage] widget directly. -class ProductPageScreen extends StatelessWidget { - /// Constructor for the product page screen. - const ProductPageScreen({ - required this.configuration, - this.initialBuildShopId, - super.key, - }); - - /// Configuration for the product page. - final ProductPageConfiguration configuration; - - /// An optional initial shop ID to select. This overrides the initialShopId - /// from the configuration. - final String? initialBuildShopId; - - @override - Widget build(BuildContext context) => Scaffold( - appBar: configuration.appBar!.call(context), - body: SafeArea( - child: ProductPage( - configuration: configuration, - initialBuildShopId: initialBuildShopId, - ), - ), - bottomNavigationBar: configuration.bottomNavigationBar, - ); -} diff --git a/packages/flutter_product_page/lib/src/ui/widgets/horizontal_list_items.dart b/packages/flutter_product_page/lib/src/widgets/horizontal_list_items.dart similarity index 93% rename from packages/flutter_product_page/lib/src/ui/widgets/horizontal_list_items.dart rename to packages/flutter_product_page/lib/src/widgets/horizontal_list_items.dart index 515f17b..bdd6495 100644 --- a/packages/flutter_product_page/lib/src/ui/widgets/horizontal_list_items.dart +++ b/packages/flutter_product_page/lib/src/widgets/horizontal_list_items.dart @@ -1,5 +1,5 @@ import "package:flutter/material.dart"; -import "package:flutter_product_page/flutter_product_page.dart"; +import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; /// Horizontal list of items. class HorizontalListItems extends StatelessWidget { @@ -14,7 +14,7 @@ class HorizontalListItems extends StatelessWidget { }); /// List of items. - final List shops; + final List shops; /// Selected item. final String selectedItem; @@ -26,7 +26,7 @@ class HorizontalListItems extends StatelessWidget { final double paddingOnButtons; /// Callback when an item is tapped. - final Function(ProductPageShop shop) onTap; + final Function(Shop shop) onTap; @override Widget build(BuildContext context) { diff --git a/packages/flutter_product_page/lib/src/ui/components/product_item.dart b/packages/flutter_product_page/lib/src/widgets/product_item.dart similarity index 89% rename from packages/flutter_product_page/lib/src/ui/components/product_item.dart rename to packages/flutter_product_page/lib/src/widgets/product_item.dart index ffcea2c..cddcecd 100644 --- a/packages/flutter_product_page/lib/src/ui/components/product_item.dart +++ b/packages/flutter_product_page/lib/src/widgets/product_item.dart @@ -1,6 +1,7 @@ import "package:cached_network_image/cached_network_image.dart"; import "package:flutter/material.dart"; -import "package:flutter_shopping/flutter_shopping.dart"; +import "package:flutter_product_page/flutter_product_page.dart"; +import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; import "package:skeletonizer/skeletonizer.dart"; /// Product item widget. @@ -10,7 +11,7 @@ class ProductItem extends StatelessWidget { required this.product, required this.onProductDetail, required this.onAddToCart, - required this.localizations, + required this.translations, super.key, }); @@ -18,13 +19,17 @@ class ProductItem extends StatelessWidget { final Product product; /// Function to call when the product detail is requested. - final Function(BuildContext context, Product selectedProduct) onProductDetail; + final Function( + BuildContext context, + Product selectedProduct, + String closeText, + ) onProductDetail; /// Function to call when the product is added to the cart. final Function(Product selectedProduct) onAddToCart; /// Localizations for the product page. - final ProductPageLocalization localizations; + final ProductPageTranslations translations; /// Size of the product image. static const double imageSize = 44; @@ -46,7 +51,7 @@ class ProductItem extends StatelessWidget { fit: BoxFit.cover, placeholder: (context, url) => loadingImageSkeleton, errorWidget: (context, url, error) => Tooltip( - message: localizations.failedToLoadImageExplenation, + message: translations.failedToLoadImageExplenation, child: Container( width: 48, height: 48, @@ -74,7 +79,11 @@ class ProductItem extends StatelessWidget { var productInformationIcon = Padding( padding: const EdgeInsets.only(left: 4), child: IconButton( - onPressed: () => onProductDetail(context, product), + onPressed: () => onProductDetail( + context, + product, + translations.close, + ), icon: Icon( Icons.info_outline, color: theme.colorScheme.primary, diff --git a/packages/flutter_product_page/lib/src/ui/widgets/product_item_popup.dart b/packages/flutter_product_page/lib/src/widgets/product_item_popup.dart similarity index 90% rename from packages/flutter_product_page/lib/src/ui/widgets/product_item_popup.dart rename to packages/flutter_product_page/lib/src/widgets/product_item_popup.dart index 099fe49..77db847 100644 --- a/packages/flutter_product_page/lib/src/ui/widgets/product_item_popup.dart +++ b/packages/flutter_product_page/lib/src/widgets/product_item_popup.dart @@ -1,12 +1,12 @@ import "package:flutter/material.dart"; -import "package:flutter_shopping/flutter_shopping.dart"; +import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; /// A popup that displays the product item. class ProductItemPopup extends StatelessWidget { /// Constructor for the product item popup. const ProductItemPopup({ required this.product, - required this.configuration, + required this.closeText, super.key, }); @@ -14,7 +14,7 @@ class ProductItemPopup extends StatelessWidget { final Product product; /// Configuration for the product page. - final ProductPageConfiguration configuration; + final String closeText; @override Widget build(BuildContext context) { @@ -49,7 +49,7 @@ class ProductItemPopup extends StatelessWidget { vertical: 8.0, ), child: Text( - configuration.localizations.close, + closeText, style: theme.textTheme.displayLarge, ), ), diff --git a/packages/flutter_product_page/lib/src/ui/components/shop_selector.dart b/packages/flutter_product_page/lib/src/widgets/shop_selector.dart similarity index 73% rename from packages/flutter_product_page/lib/src/ui/components/shop_selector.dart rename to packages/flutter_product_page/lib/src/widgets/shop_selector.dart index c9b3b76..1894aca 100644 --- a/packages/flutter_product_page/lib/src/ui/components/shop_selector.dart +++ b/packages/flutter_product_page/lib/src/widgets/shop_selector.dart @@ -1,15 +1,14 @@ import "package:flutter/material.dart"; import "package:flutter_product_page/flutter_product_page.dart"; -import "package:flutter_product_page/src/services/selected_shop_service.dart"; -import "package:flutter_product_page/src/ui/widgets/horizontal_list_items.dart"; -import "package:flutter_product_page/src/ui/widgets/spaced_wrap.dart"; +import "package:flutter_product_page/src/widgets/horizontal_list_items.dart"; +import "package:flutter_product_page/src/widgets/spaced_wrap.dart"; +import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; /// Shop selector widget that displays a list to navigate between shops. class ShopSelector extends StatelessWidget { /// Constructor for the shop selector. const ShopSelector({ required this.configuration, - required this.selectedShopService, required this.shops, required this.onTap, this.paddingBetweenButtons = 4, @@ -21,13 +20,12 @@ class ShopSelector extends StatelessWidget { final ProductPageConfiguration configuration; /// Service for the selected shop. - final SelectedShopService selectedShopService; /// List of shops. - final List shops; + final List shops; /// Callback when a shop is tapped. - final Function(ProductPageShop shop) onTap; + final Function(Shop shop) onTap; /// Padding between the buttons. final double paddingBetweenButtons; @@ -44,7 +42,8 @@ class ShopSelector extends StatelessWidget { if (configuration.shopSelectorStyle == ShopSelectorStyle.spacedWrap) { return SpacedWrap( shops: shops, - selectedItem: selectedShopService.selectedShop!.id, + selectedItem: + configuration.shoppingService.shopService.selectedShop!.id, onTap: onTap, width: MediaQuery.of(context).size.width - (16 * 2), paddingBetweenButtons: paddingBetweenButtons, @@ -54,7 +53,7 @@ class ShopSelector extends StatelessWidget { return HorizontalListItems( shops: shops, - selectedItem: selectedShopService.selectedShop!.id, + selectedItem: configuration.shoppingService.shopService.selectedShop!.id, onTap: onTap, paddingBetweenButtons: paddingBetweenButtons, paddingOnButtons: paddingOnButtons, diff --git a/packages/flutter_product_page/lib/src/ui/widgets/spaced_wrap.dart b/packages/flutter_product_page/lib/src/widgets/spaced_wrap.dart similarity index 93% rename from packages/flutter_product_page/lib/src/ui/widgets/spaced_wrap.dart rename to packages/flutter_product_page/lib/src/widgets/spaced_wrap.dart index 66cf0c8..24b19c5 100644 --- a/packages/flutter_product_page/lib/src/ui/widgets/spaced_wrap.dart +++ b/packages/flutter_product_page/lib/src/widgets/spaced_wrap.dart @@ -1,5 +1,5 @@ import "package:flutter/material.dart"; -import "package:flutter_product_page/flutter_product_page.dart"; +import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; /// SpacedWrap is a widget that wraps a list of items that are spaced out and /// fill the available width. @@ -16,7 +16,7 @@ class SpacedWrap extends StatelessWidget { }); /// List of items. - final List shops; + final List shops; /// Selected item. final String selectedItem; @@ -31,7 +31,7 @@ class SpacedWrap extends StatelessWidget { final double paddingOnButtons; /// Callback when an item is tapped. - final Function(ProductPageShop shop) onTap; + final Function(Shop shop) onTap; @override Widget build(BuildContext context) { diff --git a/packages/flutter_product_page/lib/src/ui/components/weekly_discount.dart b/packages/flutter_product_page/lib/src/widgets/weekly_discount.dart similarity index 90% rename from packages/flutter_product_page/lib/src/ui/components/weekly_discount.dart rename to packages/flutter_product_page/lib/src/widgets/weekly_discount.dart index e82a270..79d0cc9 100644 --- a/packages/flutter_product_page/lib/src/ui/components/weekly_discount.dart +++ b/packages/flutter_product_page/lib/src/widgets/weekly_discount.dart @@ -1,6 +1,7 @@ import "package:cached_network_image/cached_network_image.dart"; import "package:flutter/material.dart"; -import "package:flutter_shopping/flutter_shopping.dart"; +import "package:flutter_product_page/flutter_product_page.dart"; +import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; /// A widget that displays a weekly discount. class WeeklyDiscount extends StatelessWidget { @@ -27,7 +28,7 @@ class WeeklyDiscount extends StatelessWidget { var bottomText = Padding( padding: const EdgeInsets.all(20.0), child: Text( - configuration.getDiscountDescription!(product), + configuration.discountDescription(product), style: theme.textTheme.bodyMedium, textAlign: TextAlign.left, ), @@ -50,7 +51,7 @@ class WeeklyDiscount extends StatelessWidget { Icons.error_outline_rounded, color: Colors.red, ), - Text(configuration.localizations.failedToLoadImageExplenation), + Text(configuration.translations.failedToLoadImageExplenation), ], ), ), @@ -86,7 +87,7 @@ class WeeklyDiscount extends StatelessWidget { horizontal: 16, ), child: Text( - configuration.localizations.discountTitle, + configuration.translations.discountTitle, style: theme.textTheme.headlineSmall, textAlign: TextAlign.left, ), diff --git a/packages/flutter_product_page/pubspec.yaml b/packages/flutter_product_page/pubspec.yaml index 2207ff0..8477f9d 100644 --- a/packages/flutter_product_page/pubspec.yaml +++ b/packages/flutter_product_page/pubspec.yaml @@ -1,10 +1,10 @@ name: flutter_product_page description: "A Flutter module for the product page" -publish_to: 'none' +publish_to: "none" version: 2.0.0 environment: - sdk: '>=3.3.4 <4.0.0' + sdk: ">=3.3.4 <4.0.0" dependencies: flutter: @@ -15,11 +15,17 @@ dependencies: git: url: https://github.com/Iconica-Development/flutter_nested_categories ref: 0.0.1 - flutter_shopping: + flutter_shopping_interface: git: url: https://github.com/Iconica-Development/flutter_shopping - path: packages/flutter_shopping + path: packages/flutter_shopping_interface ref: 2.0.0 + collection: ^1.18.0 + provider: ^6.1.2 + +dependency_overrides: + flutter_shopping_interface: + path: ../flutter_shopping_interface dev_dependencies: flutter_test: diff --git a/packages/flutter_shopping_cart/lib/src/config/shopping_cart_config.dart b/packages/flutter_shopping_cart/lib/src/config/shopping_cart_config.dart index 9c080ad..f3d3cac 100644 --- a/packages/flutter_shopping_cart/lib/src/config/shopping_cart_config.dart +++ b/packages/flutter_shopping_cart/lib/src/config/shopping_cart_config.dart @@ -95,90 +95,109 @@ Widget _defaultProductItemBuilder( ShoppingCartConfig configuration, ) { var theme = Theme.of(context); - return Padding( - padding: const EdgeInsets.only(bottom: 20), - child: ListTile( - contentPadding: const EdgeInsets.only(top: 3, left: 4, bottom: 3), - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - product.name, - style: theme.textTheme.titleMedium, - ), - IconButton( - onPressed: () async { - await showModalBottomSheet( - context: context, - backgroundColor: theme.colorScheme.surface, - builder: (context) => ProductItemPopup( - product: product, - configuration: configuration, - ), - ); - }, - icon: Icon( - Icons.info_outline, - color: theme.colorScheme.primary, + return ListenableBuilder( + listenable: configuration.service, + builder: (context, _) => Padding( + padding: const EdgeInsets.only(bottom: 20), + child: ListTile( + contentPadding: const EdgeInsets.only(top: 3, left: 4, bottom: 3), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + style: theme.textTheme.titleMedium, ), - ), - ], - ), - leading: ClipRRect( - borderRadius: BorderRadius.circular(6), - child: Image.network( - product.imageUrl, + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () async { + await showModalBottomSheet( + context: context, + backgroundColor: theme.colorScheme.surface, + builder: (context) => ProductItemPopup( + product: product, + configuration: configuration, + ), + ); + }, + icon: Icon( + Icons.info_outline, + color: theme.colorScheme.primary, + ), + ), + ], ), - ), - trailing: Column( - children: [ - Text( - product.price.toStringAsFixed(2), - style: theme.textTheme.labelSmall, + leading: ClipRRect( + borderRadius: BorderRadius.circular(6), + child: Image.network( + product.imageUrl, ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - constraints: const BoxConstraints(), - padding: EdgeInsets.zero, - icon: const Icon( - Icons.remove, - color: Colors.black, - ), - onPressed: () => - configuration.service.removeOneProduct(product), - ), - Padding( - padding: const EdgeInsets.all(2), - child: Container( - alignment: Alignment.center, - decoration: BoxDecoration( - color: theme.colorScheme.primary, - borderRadius: BorderRadius.circular(4), + ), + trailing: Column( + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (product.hasDiscount && product.discountPrice != null) ...[ + Text( + product.discountPrice!.toStringAsFixed(2), + style: theme.textTheme.labelSmall, ), - height: 30, - width: 30, - child: Text( - "${product.quantity}", - style: theme.textTheme.titleSmall, - textAlign: TextAlign.center, + ] else ...[ + Text( + product.price.toStringAsFixed(2), + style: theme.textTheme.labelSmall, + ), + ], + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + constraints: const BoxConstraints(), + padding: EdgeInsets.zero, + icon: const Icon( + Icons.remove, + color: Colors.black, + ), + onPressed: () => + configuration.service.removeOneProduct(product), + ), + Padding( + padding: const EdgeInsets.all(2), + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: theme.colorScheme.primary, + borderRadius: BorderRadius.circular(4), + ), + height: 30, + width: 30, + child: Text( + "${product.quantity}", + style: theme.textTheme.titleSmall, + textAlign: TextAlign.center, + ), ), ), - ), - IconButton( - constraints: const BoxConstraints(), - padding: EdgeInsets.zero, - icon: const Icon( - Icons.add, - color: Colors.black, + IconButton( + constraints: const BoxConstraints(), + padding: EdgeInsets.zero, + icon: const Icon( + Icons.add, + color: Colors.black, + ), + onPressed: () { + configuration.service.addProduct(product); + }, ), - onPressed: () => configuration.service.addProduct(product), - ), - ], - ), - ], + ], + ), + ], + ), ), ), ); @@ -190,9 +209,12 @@ Widget _defaultSumBottomSheetBuilder( ) { var theme = Theme.of(context); - var totalPrice = configuration.service.products - .map((product) => product.price * product.quantity) - .fold(0.0, (a, b) => a + b); + var totalPrice = configuration.service.products.fold( + 0, + (previousValue, element) => + previousValue + + (element.discountPrice ?? element.price) * element.quantity, + ); return Padding( padding: configuration.bottomPadding, @@ -218,15 +240,16 @@ Widget _defaultConfirmOrderButton( Function(List products) onConfirmOrder, ) { var theme = Theme.of(context); - return Padding( padding: const EdgeInsets.symmetric(horizontal: 60), child: SizedBox( width: double.infinity, child: FilledButton( - onPressed: () => onConfirmOrder( - configuration.service.products, - ), + onPressed: configuration.service.products.isEmpty + ? null + : () => onConfirmOrder( + configuration.service.products, + ), style: theme.filledButtonTheme.style?.copyWith( backgroundColor: WidgetStateProperty.all( theme.colorScheme.primary, diff --git a/packages/flutter_shopping_cart/lib/src/widgets/shopping_cart_screen.dart b/packages/flutter_shopping_cart/lib/src/widgets/shopping_cart_screen.dart index 5d79166..4e8db4d 100644 --- a/packages/flutter_shopping_cart/lib/src/widgets/shopping_cart_screen.dart +++ b/packages/flutter_shopping_cart/lib/src/widgets/shopping_cart_screen.dart @@ -16,57 +16,6 @@ class ShoppingCartScreen extends StatelessWidget { Widget build(BuildContext context) { var theme = Theme.of(context); - var productBuilder = SingleChildScrollView( - child: Column( - children: [ - if (configuration.titleBuilder != null) ...{ - configuration.titleBuilder!( - context, - configuration.translations.cartTitle, - ), - } else ...{ - Padding( - padding: const EdgeInsets.symmetric( - vertical: 32, - ), - child: Row( - children: [ - Text( - configuration.translations.cartTitle, - style: theme.textTheme.titleLarge, - textAlign: TextAlign.start, - ), - ], - ), - ), - }, - ListenableBuilder( - listenable: configuration.service, - builder: (context, _) { - var products = configuration.service.products; - - return Column( - children: [ - for (var product in products) - configuration.productItemBuilder( - context, - product, - configuration, - ), - // Additional whitespace at the bottom to make sure the - // last product(s) are not hidden by the bottom sheet. - SizedBox( - height: configuration.confirmOrderButtonHeight + - configuration.sumBottomSheetHeight, - ), - ], - ); - }, - ), - ], - ), - ); - return Scaffold( appBar: configuration.appBar.call(context), body: SafeArea( @@ -75,7 +24,54 @@ class ShoppingCartScreen extends StatelessWidget { children: [ Padding( padding: configuration.pagePadding, - child: productBuilder, + child: SingleChildScrollView( + child: Column( + children: [ + if (configuration.titleBuilder != null) ...{ + configuration.titleBuilder!( + context, + configuration.translations.cartTitle, + ), + } else ...{ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 32, + ), + child: Row( + children: [ + Text( + configuration.translations.cartTitle, + style: theme.textTheme.titleLarge, + textAlign: TextAlign.start, + ), + ], + ), + ), + }, + ListenableBuilder( + listenable: configuration.service, + builder: (context, _) => Column( + children: [ + for (var product in configuration.service.products) + configuration.productItemBuilder( + context, + product, + configuration, + ), + + // Additional whitespace at + // the bottom to make sure the last + // product(s) are not hidden by the bottom sheet. + SizedBox( + height: configuration.confirmOrderButtonHeight + + configuration.sumBottomSheetHeight, + ), + ], + ), + ), + ], + ), + ), ), Align( alignment: Alignment.bottomCenter, @@ -108,8 +104,7 @@ class _BottomSheet extends StatelessWidget { ), ListenableBuilder( listenable: configuration.service, - builder: (BuildContext context, Widget? child) => - configuration.confirmOrderButtonBuilder( + builder: (context, _) => configuration.confirmOrderButtonBuilder( context, configuration, configuration.onConfirmOrder,