diff --git a/.github/labeler.yml b/.github/labeler.yml index 93715980256..8aa4e9d1757 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -258,18 +258,22 @@ Prices: - any-glob-to-any-file: 'packages/smooth_app/lib/database/dao_osm_location.dart' - any-glob-to-any-file: 'packages/smooth_app/lib/pages/locations/location_map_page.dart' - any-glob-to-any-file: 'packages/smooth_app/lib/pages/locations/search_location_preloaded_item.dart' + - any-glob-to-any-file: 'packages/smooth_app/lib/pages/prices/get_prices_model.dart' - any-glob-to-any-file: 'packages/smooth_app/lib/pages/prices/price_amount_card.dart' - any-glob-to-any-file: 'packages/smooth_app/lib/pages/prices/price_amount_field.dart' + - any-glob-to-any-file: 'packages/smooth_app/lib/pages/prices/price_button.dart' + - any-glob-to-any-file: 'packages/smooth_app/lib/pages/prices/price_count_widget.dart' - any-glob-to-any-file: 'packages/smooth_app/lib/pages/prices/price_currency_card.dart' - any-glob-to-any-file: 'packages/smooth_app/lib/pages/prices/price_currency_selector.dart' + - any-glob-to-any-file: 'packages/smooth_app/lib/pages/prices/price_data_widget.dart' - any-glob-to-any-file: 'packages/smooth_app/lib/pages/prices/price_date_card.dart' - any-glob-to-any-file: 'packages/smooth_app/lib/pages/prices/price_location_card.dart' - any-glob-to-any-file: 'packages/smooth_app/lib/pages/prices/price_model.dart' + - any-glob-to-any-file: 'packages/smooth_app/lib/pages/prices/price_product_widget.dart' + - any-glob-to-any-file: 'packages/smooth_app/lib/pages/prices/prices_page.dart' - any-glob-to-any-file: 'packages/smooth_app/lib/pages/prices/price_proof_card.dart' - any-glob-to-any-file: 'packages/smooth_app/lib/pages/prices/prices_card.dart' - any-glob-to-any-file: 'packages/smooth_app/lib/pages/prices/product_price_add_page.dart' - - any-glob-to-any-file: 'packages/smooth_app/lib/pages/prices/product_price_item.dart' - - any-glob-to-any-file: 'packages/smooth_app/lib/pages/prices/product_prices_page.dart' - any-glob-to-any-file: 'packages/smooth_app/lib/pages/onboarding/currency_selector.dart' - any-glob-to-any-file: 'packages/smooth_app/lib/pages/onboarding/currency_selector_helper.dart' diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart index 9674a67dff7..beddecb5374 100644 --- a/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart @@ -1,3 +1,4 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; @@ -16,6 +17,8 @@ import 'package:smooth_app/pages/preferences/account_deletion_webview.dart'; import 'package:smooth_app/pages/preferences/user_preferences_item.dart'; import 'package:smooth_app/pages/preferences/user_preferences_list_tile.dart'; import 'package:smooth_app/pages/preferences/user_preferences_page.dart'; +import 'package:smooth_app/pages/prices/get_prices_model.dart'; +import 'package:smooth_app/pages/prices/prices_page.dart'; import 'package:smooth_app/pages/product/common/product_query_page_helper.dart'; import 'package:smooth_app/pages/user_management/login_page.dart'; import 'package:smooth_app/query/paged_product_query.dart'; @@ -210,9 +213,35 @@ class UserPreferencesAccount extends AbstractUserPreferences { localDatabase: localDatabase, myCount: _getMyCount(UserSearchType.TO_BE_COMPLETED), ), - _getPriceListTile( + _getListTile( appLocalizations.user_search_prices_title, - 'app/dashboard/prices', + () async => Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => PricesPage( + GetPricesModel( + parameters: GetPricesParameters() + ..owner = userId + ..orderBy = >[ + const OrderBy( + field: GetPricesOrderField.created, + ascending: false, + ), + ] + ..pageSize = GetPricesModel.pageSize + ..pageNumber = 1, + displayOwner: false, + displayProduct: true, + uri: OpenPricesAPIClient.getUri( + path: 'app/users/${ProductQuery.getWriteUser().userId}', + uriHelper: ProductQuery.uriProductHelper, + ), + title: appLocalizations.user_search_prices_title, + subtitle: ProductQuery.getWriteUser().userId, + ), + ), + ), + ), + CupertinoIcons.money_dollar_circle, myCount: _getMyPricesCount(), ), _getPriceListTile( diff --git a/packages/smooth_app/lib/pages/prices/get_prices_model.dart b/packages/smooth_app/lib/pages/prices/get_prices_model.dart new file mode 100644 index 00000000000..1b91fcb83f6 --- /dev/null +++ b/packages/smooth_app/lib/pages/prices/get_prices_model.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; + +/// Model that stores what we need to know for "get latest prices" queries. +class GetPricesModel { + const GetPricesModel({ + required this.parameters, + required this.displayOwner, + required this.displayProduct, + required this.uri, + required this.title, + this.subtitle, + this.addButton, + }); + + /// Query parameters. + final GetPricesParameters parameters; + + /// Should we display the owner for each price? No if it's an owner query. + final bool displayOwner; + + /// Should we display the product for each price? No if it's a product query. + final bool displayProduct; + + /// Related web app URI. + final Uri uri; + + /// Page title. + final String title; + + /// Page subtitle. + final String? subtitle; + + /// "Add a price" callback. + final VoidCallback? addButton; + + static const int pageSize = 10; +} diff --git a/packages/smooth_app/lib/pages/prices/price_button.dart b/packages/smooth_app/lib/pages/prices/price_button.dart new file mode 100644 index 00000000000..d07bf5fcfd4 --- /dev/null +++ b/packages/smooth_app/lib/pages/prices/price_button.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +/// Simple price button: displaying data with optional action. +class PriceButton extends StatelessWidget { + const PriceButton({ + this.title, + this.iconData, + this.buttonStyle, + required this.onPressed, + }); + + final String? title; + final IconData? iconData; + final ButtonStyle? buttonStyle; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + if (iconData == null) { + return ElevatedButton( + onPressed: onPressed, + style: buttonStyle, + child: Text(title!), + ); + } + if (title == null) { + return ElevatedButton( + onPressed: onPressed, + style: buttonStyle, + child: Icon(iconData), + ); + } + return ElevatedButton.icon( + onPressed: onPressed, + icon: Icon(iconData), + label: Text(title!), + style: buttonStyle, + ); + } +} diff --git a/packages/smooth_app/lib/pages/prices/price_count_widget.dart b/packages/smooth_app/lib/pages/prices/price_count_widget.dart new file mode 100644 index 00000000000..f703a7b1da4 --- /dev/null +++ b/packages/smooth_app/lib/pages/prices/price_count_widget.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:smooth_app/pages/prices/price_button.dart'; + +/// Price Count display. +class PriceCountWidget extends StatelessWidget { + const PriceCountWidget(this.count); + + final int count; + + @override + Widget build(BuildContext context) => PriceButton( + onPressed: null, + iconData: Icons.label, + title: '$count', + buttonStyle: ElevatedButton.styleFrom( + disabledForegroundColor: _getForegroundColor(), + disabledBackgroundColor: _getBackgroundColor(), + ), + ); + + Color? _getForegroundColor() => switch (count) { + 0 => Colors.red, + 1 => Colors.orange, + _ => Colors.green, + }; + + Color? _getBackgroundColor() => switch (count) { + 0 => Colors.red[100], + 1 => Colors.orange[100], + _ => Colors.green[100], + }; +} diff --git a/packages/smooth_app/lib/pages/prices/product_price_item.dart b/packages/smooth_app/lib/pages/prices/price_data_widget.dart similarity index 54% rename from packages/smooth_app/lib/pages/prices/product_price_item.dart rename to packages/smooth_app/lib/pages/prices/price_data_widget.dart index 562ac832c37..62e0eee983d 100644 --- a/packages/smooth_app/lib/pages/prices/product_price_item.dart +++ b/packages/smooth_app/lib/pages/prices/price_data_widget.dart @@ -3,17 +3,22 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:intl/intl.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; -import 'package:smooth_app/generic_lib/widgets/smooth_card.dart'; import 'package:smooth_app/helpers/launch_url_helper.dart'; import 'package:smooth_app/pages/prices/emoji_helper.dart'; +import 'package:smooth_app/pages/prices/get_prices_model.dart'; +import 'package:smooth_app/pages/prices/price_button.dart'; import 'package:smooth_app/pages/product/common/product_query_page_helper.dart'; import 'package:smooth_app/query/product_query.dart'; -/// Single product price widget. -class ProductPriceItem extends StatelessWidget { - const ProductPriceItem(this.price); +/// Price Data display (no product data here). +class PriceDataWidget extends StatelessWidget { + const PriceDataWidget( + this.price, { + required this.model, + }); final Price price; + final GetPricesModel model; @override Widget build(BuildContext context) { @@ -31,7 +36,7 @@ class ProductPriceItem extends StatelessWidget { if (price.product == null) { return null; } - if (price.product!.quantityUnit != 'g') { + if ((price.product!.quantityUnit ?? 'g') != 'g') { return null; } return '${currencyFormat.format(price.price / (price.product!.quantity! / 1000))} / kg'; @@ -52,62 +57,59 @@ class ProductPriceItem extends StatelessWidget { final String? pricePerKg = getPricePerKg(); final String? notDiscountedPrice = getNotDiscountedPrice(); - return SmoothCard( - child: ListTile( - title: Text( + + return Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: MEDIUM_SPACE, + children: [ + Text( '${currencyFormat.format(price.price)}' - '${pricePerKg == null ? '' : ' ($pricePerKg)'}' - ' ' - '${dateFormat.format(price.date)}' - '${notDiscountedPrice == null ? '' : ' ($notDiscountedPrice)'}', + ' ${pricePerKg == null ? '' : ' ($pricePerKg)'}', ), - subtitle: Wrap( - spacing: MEDIUM_SPACE, - children: [ - if (locationTitle != null) - ElevatedButton.icon( - // TODO(monsieurtanuki): open a still-to-be-done "price x location" page - onPressed: () {}, - icon: const Icon(Icons.location_on_outlined), - label: Text(locationTitle), - ), - ElevatedButton.icon( - // TODO(monsieurtanuki): open a still-to-be-done "price x user" page - onPressed: () {}, - icon: const Icon(Icons.account_box), - label: Text(price.owner), - ), - Tooltip( - message: '${dateFormat.format(price.created)}' - ' ' - '${timeFormat.format(price.created)}', - child: ElevatedButton.icon( - // TODO(monsieurtanuki): misleading "active" button - onPressed: () {}, - icon: const Icon(Icons.history), - label: Text( - ProductQueryPageHelper.getDurationStringFromTimestamp( - price.created.millisecondsSinceEpoch, - context, - compact: true, - ), - ), - ), + Text(dateFormat.format(price.date)), + if (notDiscountedPrice != null) Text('($notDiscountedPrice)'), + if (locationTitle != null) + // TODO(monsieurtanuki): open a still-to-be-done "price x location" page + PriceButton( + title: locationTitle, + iconData: Icons.location_on_outlined, + onPressed: () {}, + ), + if (model.displayOwner) + PriceButton( + // TODO(monsieurtanuki): open a still-to-be-done "price x owner" page + title: price.owner, + iconData: Icons.account_box, + onPressed: () {}, + ), + Tooltip( + message: '${dateFormat.format(price.created)}' + ' ' + '${timeFormat.format(price.created)}', + child: PriceButton( + // TODO(monsieurtanuki): misleading "active" button + onPressed: () {}, + iconData: Icons.history, + title: ProductQueryPageHelper.getDurationStringFromTimestamp( + price.created.millisecondsSinceEpoch, + context, + compact: true, ), - if (price.proof?.filePath != null) - ElevatedButton( - onPressed: () async => LaunchUrlHelper.launchURL( - price.proof! - .getFileUrl( - uriProductHelper: ProductQuery.uriProductHelper, - ) - .toString(), - ), - child: const Icon(Icons.image), - ), - ], + ), ), - ), + if (price.proof?.filePath != null) + PriceButton( + iconData: Icons.image, + onPressed: () async => LaunchUrlHelper.launchURL( + price.proof! + .getFileUrl( + uriProductHelper: ProductQuery.uriProductHelper, + ) + .toString(), + ), + ), + ], ); } diff --git a/packages/smooth_app/lib/pages/prices/price_product_widget.dart b/packages/smooth_app/lib/pages/prices/price_product_widget.dart new file mode 100644 index 00000000000..6fca9259a4f --- /dev/null +++ b/packages/smooth_app/lib/pages/prices/price_product_widget.dart @@ -0,0 +1,81 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; +import 'package:smooth_app/generic_lib/design_constants.dart'; +import 'package:smooth_app/generic_lib/widgets/images/smooth_image.dart'; +import 'package:smooth_app/pages/prices/price_button.dart'; +import 'package:smooth_app/pages/prices/price_count_widget.dart'; + +/// Price Product display (no price data here). +class PriceProductWidget extends StatelessWidget { + const PriceProductWidget(this.priceProduct); + + final PriceProduct priceProduct; + + static const double _imageSize = 75; + + @override + Widget build(BuildContext context) { + final String? name = priceProduct.name; + final String? imageURL = priceProduct.imageURL; + final int priceCount = priceProduct.priceCount; + final List? brands = priceProduct.brands?.split(','); + final String? quantity = priceProduct.quantity == null + ? null + : '${priceProduct.quantity} ${priceProduct.quantityUnit ?? 'g'}'; + return LayoutBuilder( + builder: ( + final BuildContext context, + final BoxConstraints constraints, + ) => + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (imageURL != null) + SizedBox( + width: _imageSize, + child: SmoothImage( + width: _imageSize, + height: _imageSize, + imageProvider: NetworkImage(imageURL), + ), + ), + if (imageURL != null) const SizedBox(width: SMALL_SPACE), + SizedBox( + width: imageURL == null + ? constraints.maxWidth + : constraints.maxWidth - _imageSize - SMALL_SPACE, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (name != null) + AutoSizeText( + name, + maxLines: 2, + style: Theme.of(context).textTheme.titleMedium, + ), + Wrap( + spacing: VERY_SMALL_SPACE, + crossAxisAlignment: WrapCrossAlignment.center, + runSpacing: 0, + children: [ + PriceCountWidget(priceCount), + if (brands != null) + for (final String brand in brands) + PriceButton( + title: brand, + onPressed: () {}, + ), + if (quantity != null) Text(quantity), + ], + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/packages/smooth_app/lib/pages/prices/prices_card.dart b/packages/smooth_app/lib/pages/prices/prices_card.dart index 6895a524cc8..a4b5968dffb 100644 --- a/packages/smooth_app/lib/pages/prices/prices_card.dart +++ b/packages/smooth_app/lib/pages/prices/prices_card.dart @@ -5,8 +5,10 @@ import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:smooth_app/generic_lib/buttons/smooth_large_button_with_icon.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/helpers/product_cards_helper.dart'; +import 'package:smooth_app/pages/prices/get_prices_model.dart'; +import 'package:smooth_app/pages/prices/prices_page.dart'; import 'package:smooth_app/pages/prices/product_price_add_page.dart'; -import 'package:smooth_app/pages/prices/product_prices_page.dart'; +import 'package:smooth_app/query/product_query.dart'; /// Card that displays buttons related to prices. class PricesCard extends StatelessWidget { @@ -37,8 +39,34 @@ class PricesCard extends StatelessWidget { icon: CupertinoIcons.tag_fill, onPressed: () async => Navigator.of(context).push( MaterialPageRoute( - builder: (BuildContext context) => - ProductPricesPage(product), + builder: (BuildContext context) => PricesPage( + GetPricesModel( + parameters: GetPricesParameters() + ..productCode = product.barcode + ..orderBy = >[ + const OrderBy( + field: GetPricesOrderField.created, + ascending: false, + ), + ] + ..pageSize = GetPricesModel.pageSize + ..pageNumber = 1, + displayOwner: true, + displayProduct: false, + uri: OpenPricesAPIClient.getUri( + path: 'app/products/${product.barcode!}', + uriHelper: ProductQuery.uriProductHelper, + ), + title: getProductNameAndBrands( + product, + appLocalizations, + ), + addButton: () async => ProductPriceAddPage.showPage( + context: context, + product: product, + ), + ), + ), ), ), ), diff --git a/packages/smooth_app/lib/pages/prices/prices_page.dart b/packages/smooth_app/lib/pages/prices/prices_page.dart new file mode 100644 index 00000000000..51e9201657b --- /dev/null +++ b/packages/smooth_app/lib/pages/prices/prices_page.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:smooth_app/generic_lib/widgets/smooth_back_button.dart'; +import 'package:smooth_app/helpers/launch_url_helper.dart'; +import 'package:smooth_app/pages/prices/get_prices_model.dart'; +import 'package:smooth_app/pages/prices/product_prices_list.dart'; +import 'package:smooth_app/widgets/smooth_app_bar.dart'; +import 'package:smooth_app/widgets/smooth_scaffold.dart'; + +/// Page that displays the latest prices according to a model. +class PricesPage extends StatelessWidget { + const PricesPage(this.model); + + final GetPricesModel model; + + @override + Widget build(BuildContext context) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + return SmoothScaffold( + appBar: SmoothAppBar( + centerTitle: false, + leading: const SmoothBackButton(), + title: Text( + model.title, + maxLines: model.subtitle == null ? 2 : 1, + ), + subTitle: model.subtitle == null ? null : Text(model.subtitle!), + actions: [ + IconButton( + tooltip: appLocalizations.prices_app_button, + icon: const Icon(Icons.open_in_new), + onPressed: () async => LaunchUrlHelper.launchURL( + model.uri.toString(), + ), + ), + ], + ), + body: ProductPricesList(model), + floatingActionButton: model.addButton == null + ? null + : FloatingActionButton.extended( + onPressed: model.addButton, + label: Text(appLocalizations.prices_add_a_price), + icon: const Icon(Icons.add), + ), + ); + } +} diff --git a/packages/smooth_app/lib/pages/prices/product_prices_list.dart b/packages/smooth_app/lib/pages/prices/product_prices_list.dart index 2d453f5fc95..b2b6d86de7d 100644 --- a/packages/smooth_app/lib/pages/prices/product_prices_list.dart +++ b/packages/smooth_app/lib/pages/prices/product_prices_list.dart @@ -3,14 +3,18 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_card.dart'; -import 'package:smooth_app/pages/prices/product_price_item.dart'; +import 'package:smooth_app/pages/prices/get_prices_model.dart'; +import 'package:smooth_app/pages/prices/price_data_widget.dart'; +import 'package:smooth_app/pages/prices/price_product_widget.dart'; import 'package:smooth_app/query/product_query.dart'; -/// List of the latest prices for a given product. +/// List of the latest prices for a given model. class ProductPricesList extends StatefulWidget { - const ProductPricesList(this.barcode); + const ProductPricesList( + this.model, + ); - final String barcode; + final GetPricesModel model; @override State createState() => _ProductPricesListState(); @@ -18,9 +22,7 @@ class ProductPricesList extends StatefulWidget { class _ProductPricesListState extends State { late final Future> _prices = - _showProductPrices(widget.barcode); - - static const int _pageSize = 10; + _showProductPrices(widget.model.parameters); // TODO(monsieurtanuki): add a refresh gesture // TODO(monsieurtanuki): add a "download the next 10" items @@ -51,15 +53,46 @@ class _ProductPricesListState extends State { return const Text('empty list'); } final List children = []; + + if (!widget.model.displayProduct) { + // in that case we display the product only once, if possible. + for (final Price price in result.items!) { + final PriceProduct? priceProduct = price.product; + if (priceProduct == null) { + continue; + } + children.add( + SmoothCard(child: PriceProductWidget(priceProduct)), + ); + break; + } + } + for (final Price price in result.items!) { - children.add(ProductPriceItem(price)); + final PriceProduct? priceProduct = price.product; + children.add( + SmoothCard( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.model.displayProduct && priceProduct != null) + PriceProductWidget(priceProduct), + PriceDataWidget( + price, + model: widget.model, + ), + ], + ), + ), + ); } final AppLocalizations appLocalizations = AppLocalizations.of(context); final String title = result.numberOfPages == 1 ? appLocalizations.prices_list_length_one_page(children.length) : appLocalizations.prices_list_length_many_pages( - _pageSize, + widget.model.parameters.pageSize!, result.total!, ); children.insert( @@ -77,21 +110,10 @@ class _ProductPricesListState extends State { ); static Future> _showProductPrices( - final String barcode, { - final int pageSize = _pageSize, - final int pageNumber = 1, - }) async => + final GetPricesParameters parameters, + ) async => OpenPricesAPIClient.getPrices( - GetPricesParameters() - ..productCode = barcode - ..orderBy = >[ - const OrderBy( - field: GetPricesOrderField.created, - ascending: false, - ), - ] - ..pageSize = pageSize - ..pageNumber = pageNumber, + parameters, uriHelper: ProductQuery.uriProductHelper, ); } diff --git a/packages/smooth_app/lib/pages/prices/product_prices_page.dart b/packages/smooth_app/lib/pages/prices/product_prices_page.dart deleted file mode 100644 index f299cdd99db..00000000000 --- a/packages/smooth_app/lib/pages/prices/product_prices_page.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:openfoodfacts/openfoodfacts.dart'; -import 'package:provider/provider.dart'; -import 'package:smooth_app/data_models/up_to_date_mixin.dart'; -import 'package:smooth_app/database/local_database.dart'; -import 'package:smooth_app/generic_lib/widgets/smooth_back_button.dart'; -import 'package:smooth_app/helpers/launch_url_helper.dart'; -import 'package:smooth_app/helpers/product_cards_helper.dart'; -import 'package:smooth_app/pages/prices/product_price_add_page.dart'; -import 'package:smooth_app/pages/prices/product_prices_list.dart'; -import 'package:smooth_app/query/product_query.dart'; -import 'package:smooth_app/widgets/smooth_app_bar.dart'; -import 'package:smooth_app/widgets/smooth_scaffold.dart'; - -/// Page that displays the latest prices for a given product. -class ProductPricesPage extends StatefulWidget { - const ProductPricesPage(this.product); - - final Product product; - - @override - State createState() => _ProductPricesPageState(); -} - -class _ProductPricesPageState extends State - with UpToDateMixin { - @override - void initState() { - super.initState(); - initUpToDate(widget.product, context.read()); - } - - @override - Widget build(BuildContext context) { - final AppLocalizations appLocalizations = AppLocalizations.of(context); - context.watch(); - refreshUpToDate(); - - return SmoothScaffold( - appBar: SmoothAppBar( - centerTitle: false, - leading: const SmoothBackButton(), - title: Text( - getProductNameAndBrands(upToDateProduct, appLocalizations), - maxLines: 2, - ), - actions: [ - IconButton( - tooltip: appLocalizations.prices_app_button, - icon: const Icon(Icons.open_in_new), - onPressed: () async => LaunchUrlHelper.launchURL( - OpenPricesAPIClient.getUri( - path: 'app/products/${upToDateProduct.barcode!}', - uriHelper: ProductQuery.uriProductHelper, - ).toString(), - ), - ), - ], - ), - body: ProductPricesList(barcode), - floatingActionButton: FloatingActionButton.extended( - onPressed: () async => ProductPriceAddPage.showPage( - context: context, - product: upToDateProduct, - ), - label: Text(appLocalizations.prices_add_a_price), - icon: const Icon(Icons.add), - ), - ); - } -}