diff --git a/.github/labeler.yml b/.github/labeler.yml index 7b1233f9519c..5c4c8fcfed23 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -279,7 +279,6 @@ Prices: - 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' diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index 2c282baadd87..88cd78852249 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -2011,6 +2011,54 @@ } } }, + "prices_locations_list_length_many_pages": "Top {pageSize} locations (total: {total})", + "@prices_locations_list_length_many_pages": { + "description": "Number of locations for one-page result", + "placeholders": { + "pageSize": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "prices_button_count_proof": "{count,plural, =0{No proof} =1{One proof} other{{count} proofs}}", + "@prices_button_count_proof": { + "description": "Number of proofs, for a button", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "prices_button_count_product": "{count,plural, =0{No product} =1{One product} other{{count} products}}", + "@prices_button_count_product": { + "description": "Number of products, for a button", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "prices_button_count_user": "{count,plural, =0{No user} =1{One user} other{{count} users}}", + "@prices_button_count_user": { + "description": "Number of users, for a button", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "prices_button_count_price": "{count,plural, =0{No price} =1{One price} other{{count} prices}}", + "@prices_button_count_price": { + "description": "Number of prices, for a button", + "placeholders": { + "count": { + "type": "int" + } + } + }, "prices_amount_subtitle": "Amount", "prices_amount_is_discounted": "Is discounted?", "prices_amount_price_normal": "Price", @@ -2142,6 +2190,10 @@ "@all_search_prices_top_location_title": { "description": "Top price locations: list tile title" }, + "all_search_prices_top_location_single_title": "Prices in a store", + "@all_search_prices_top_location_single_title": { + "description": "Top price locations: list tile title" + }, "all_search_prices_top_product_title": "Products with the most prices", "@all_search_prices_top_product_title": { "description": "Top price products: list tile title" diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_prices.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_prices.dart index 8cb5528a3039..9bd2b0313e30 100644 --- a/packages/smooth_app/lib/pages/preferences/user_preferences_prices.dart +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_prices.dart @@ -10,7 +10,9 @@ 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/price_button.dart'; import 'package:smooth_app/pages/prices/price_user_button.dart'; +import 'package:smooth_app/pages/prices/prices_locations_page.dart'; import 'package:smooth_app/pages/prices/prices_page.dart'; import 'package:smooth_app/pages/prices/prices_proofs_page.dart'; import 'package:smooth_app/pages/prices/prices_users_page.dart'; @@ -87,17 +89,7 @@ class UserPreferencesPrices extends AbstractUserPreferences { MaterialPageRoute( builder: (BuildContext context) => PricesPage( GetPricesModel( - parameters: GetPricesParameters() - ..orderBy = >[ - const OrderBy( - field: GetPricesOrderField.created, - ascending: false, - ), - ] - ..pageSize = GetPricesModel.pageSize - ..pageNumber = 1, - displayOwner: true, - displayProduct: true, + parameters: GetPricesModel.getStandardPricesParameters(), uri: OpenPricesAPIClient.getUri( path: 'prices', uriHelper: ProductQuery.uriPricesHelper, @@ -118,11 +110,16 @@ class UserPreferencesPrices extends AbstractUserPreferences { builder: (BuildContext context) => const PricesUsersPage(), ), ), - Icons.account_box, + PriceButton.userIconData, ), - _getPriceListTile( + _getListTile( appLocalizations.all_search_prices_top_location_title, - 'locations', + () async => Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => const PricesLocationsPage(), + ), + ), + PriceButton.locationIconData, ), _getPriceListTile( appLocalizations.all_search_prices_top_product_title, diff --git a/packages/smooth_app/lib/pages/prices/get_prices_model.dart b/packages/smooth_app/lib/pages/prices/get_prices_model.dart index dd350af6ad4f..1b556af41a29 100644 --- a/packages/smooth_app/lib/pages/prices/get_prices_model.dart +++ b/packages/smooth_app/lib/pages/prices/get_prices_model.dart @@ -10,8 +10,9 @@ import 'package:smooth_app/query/product_query.dart'; class GetPricesModel { const GetPricesModel({ required this.parameters, - required this.displayOwner, - required this.displayProduct, + this.displayEachOwner = true, + this.displayEachProduct = true, + this.displayEachLocation = true, required this.uri, required this.title, this.lazyCounterPrices, @@ -26,10 +27,13 @@ class GetPricesModel { required final BuildContext context, }) => GetPricesModel( - parameters: _getProductPricesParameters(product.barcode), - displayOwner: true, - displayProduct: false, - uri: _getProductPricesUri(product.barcode), + parameters: getStandardPricesParameters() + ..productCode = product.barcode, + displayEachProduct: false, + uri: OpenPricesAPIClient.getUri( + path: 'products/${product.barcode}', + uriHelper: ProductQuery.uriPricesHelper, + ), title: product.getName(AppLocalizations.of(context)), subtitle: product.barcode, addButton: () async => ProductPriceAddPage.showProductPage( @@ -40,36 +44,32 @@ class GetPricesModel { enableCountButton: false, ); - static GetPricesParameters _getProductPricesParameters( - final String barcode, - ) => + static GetPricesParameters getStandardPricesParameters() => GetPricesParameters() - ..productCode = barcode - ..orderBy = >[ - const OrderBy( + ..orderBy = const >[ + OrderBy( field: GetPricesOrderField.created, ascending: false, ), + OrderBy( + field: GetPricesOrderField.date, + ascending: false, + ), ] - ..pageSize = pageSize + ..pageSize = 10 ..pageNumber = 1; - static Uri _getProductPricesUri( - final String barcode, - ) => - OpenPricesAPIClient.getUri( - path: 'products/$barcode', - uriHelper: ProductQuery.uriPricesHelper, - ); - /// Query parameters. final GetPricesParameters parameters; /// Should we display the owner for each price? No if it's an owner query. - final bool displayOwner; + final bool displayEachOwner; /// Should we display the product for each price? No if it's a product query. - final bool displayProduct; + final bool displayEachProduct; + + /// Should we display the location for each price? No if it's a location query. + final bool displayEachLocation; /// Related web app URI. final Uri uri; @@ -88,6 +88,4 @@ class GetPricesModel { /// Lazy Counter to refresh. final LazyCounterPrices? lazyCounterPrices; - - 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 index 8b68413bf5ea..63e13dd3e168 100644 --- a/packages/smooth_app/lib/pages/prices/price_button.dart +++ b/packages/smooth_app/lib/pages/prices/price_button.dart @@ -16,6 +16,14 @@ class PriceButton extends StatelessWidget { final VoidCallback? onPressed; final String? tooltip; + static const IconData priceIconData = Icons.label; + static const IconData userIconData = Icons.account_box; + static const IconData proofIconData = Icons.image; + static const IconData locationIconData = Icons.location_on; + static const IconData historyIconData = Icons.history; + static const IconData productIconData = Icons.category; + static const IconData warningIconData = Icons.warning; + @override Widget build(BuildContext context) { final Widget widget; diff --git a/packages/smooth_app/lib/pages/prices/price_count_widget.dart b/packages/smooth_app/lib/pages/prices/price_count_widget.dart index 0b1fa92a402d..436a50854d4a 100644 --- a/packages/smooth_app/lib/pages/prices/price_count_widget.dart +++ b/packages/smooth_app/lib/pages/prices/price_count_widget.dart @@ -1,71 +1,44 @@ import 'package:flutter/material.dart'; -import 'package:openfoodfacts/openfoodfacts.dart'; -import 'package:provider/provider.dart'; -import 'package:smooth_app/database/dao_product.dart'; -import 'package:smooth_app/database/local_database.dart'; -import 'package:smooth_app/pages/prices/get_prices_model.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:smooth_app/pages/prices/price_button.dart'; -import 'package:smooth_app/pages/prices/price_meta_product.dart'; -import 'package:smooth_app/pages/prices/prices_page.dart'; /// Price Count display. class PriceCountWidget extends StatelessWidget { - const PriceCountWidget( - this.count, { - required this.priceProduct, - required this.enableCountButton, + const PriceCountWidget({ + required this.count, + required this.onPressed, }); final int count; - final PriceProduct priceProduct; - final bool enableCountButton; + final VoidCallback? onPressed; @override Widget build(BuildContext context) => PriceButton( - onPressed: !enableCountButton - ? null - : () async { - final LocalDatabase localDatabase = - context.read(); - final Product? newProduct = - await DaoProduct(localDatabase).get(priceProduct.code); - if (!context.mounted) { - return; - } - return Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) => PricesPage( - GetPricesModel.product( - product: newProduct != null - ? PriceMetaProduct.product(newProduct) - : PriceMetaProduct.priceProduct(priceProduct), - context: context, - ), - ), - ), - ); - }, - iconData: Icons.label, + onPressed: onPressed, + iconData: PriceButton.priceIconData, title: '$count', buttonStyle: ElevatedButton.styleFrom( disabledForegroundColor: - enableCountButton ? null : getForegroundColor(count), + onPressed != null ? null : _getForegroundColor(count), disabledBackgroundColor: - enableCountButton ? null : getBackgroundColor(count), + onPressed != null ? null : _getBackgroundColor(count), foregroundColor: - !enableCountButton ? null : getForegroundColor(count), + onPressed == null ? null : _getForegroundColor(count), backgroundColor: - !enableCountButton ? null : getBackgroundColor(count), + onPressed == null ? null : _getBackgroundColor(count), + ), + tooltip: AppLocalizations.of(context).prices_button_count_price( + count, ), ); - static Color? getForegroundColor(final int count) => switch (count) { + static Color? _getForegroundColor(final int count) => switch (count) { 0 => Colors.red, 1 => Colors.orange, _ => Colors.green, }; - static Color? getBackgroundColor(final int count) => switch (count) { + static Color? _getBackgroundColor(final int count) => switch (count) { 0 => Colors.red[100], 1 => Colors.orange[100], _ => Colors.green[100], diff --git a/packages/smooth_app/lib/pages/prices/price_data_widget.dart b/packages/smooth_app/lib/pages/prices/price_data_widget.dart index a2fea52a97c6..fe40e2633211 100644 --- a/packages/smooth_app/lib/pages/prices/price_data_widget.dart +++ b/packages/smooth_app/lib/pages/prices/price_data_widget.dart @@ -3,9 +3,9 @@ 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/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/prices/price_location_widget.dart'; import 'package:smooth_app/pages/prices/price_proof_page.dart'; import 'package:smooth_app/pages/prices/price_user_button.dart'; import 'package:smooth_app/pages/product/common/product_query_page_helper.dart'; @@ -31,7 +31,8 @@ class PriceDataWidget extends StatelessWidget { locale: locale, name: price.currency.name, ); - final String? locationTitle = _getLocationTitle(price.location); + final String? locationTitle = + PriceLocationWidget.getLocationTitle(price.location); String? getPricePerKg() { if (price.product == null) { @@ -81,16 +82,21 @@ class PriceDataWidget extends StatelessWidget { ExcludeSemantics(child: Text(priceLabel)), ExcludeSemantics(child: Text(dateFormat.format(price.date))), if (notDiscountedPrice != null) Text('($notDiscountedPrice)'), - if (locationTitle != null) + if (model.displayEachLocation && locationTitle != null) // TODO(monsieurtanuki): open a still-to-be-done "price x location" page ExcludeSemantics( child: PriceButton( title: locationTitle, - iconData: Icons.location_on_outlined, - onPressed: () {}, + iconData: PriceButton.locationIconData, + onPressed: price.locationId == null + ? () {} + : () async => PriceLocationWidget.showLocationPrices( + locationId: price.locationId!, + context: context, + ), ), ), - if (model.displayOwner) PriceUserButton(price.owner), + if (model.displayEachOwner) PriceUserButton(price.owner), ExcludeSemantics( child: Tooltip( message: '${dateFormat.format(price.created)}' @@ -99,7 +105,7 @@ class PriceDataWidget extends StatelessWidget { child: PriceButton( // TODO(monsieurtanuki): misleading "active" button onPressed: () {}, - iconData: Icons.history, + iconData: PriceButton.historyIconData, title: ProductQueryPageHelper.getDurationStringFromTimestamp( price.created.millisecondsSinceEpoch, context, @@ -110,7 +116,7 @@ class PriceDataWidget extends StatelessWidget { ), if (price.proof?.filePath != null) PriceButton( - iconData: Icons.image, + iconData: PriceButton.proofIconData, tooltip: appLocalizations.prices_open_proof, onPressed: () async => Navigator.push( context, @@ -125,36 +131,4 @@ class PriceDataWidget extends StatelessWidget { ), ); } - - static String? _getLocationTitle(final Location? location) { - if (location == null) { - return null; - } - final StringBuffer result = StringBuffer(); - final String? countryEmoji = EmojiHelper.getCountryEmoji( - _getCountry(location), - ); - if (location.name != null) { - result.write(location.name); - } - if (location.city != null) { - if (result.isNotEmpty) { - result.write(', '); - } - result.write(location.city); - } - if (countryEmoji != null) { - if (result.isNotEmpty) { - result.write(' '); - } - result.write(countryEmoji); - } - if (result.isEmpty) { - return null; - } - return result.toString(); - } - - static OpenFoodFactsCountry? _getCountry(final Location location) => - OpenFoodFactsCountry.fromOffTag(location.countryCode); } diff --git a/packages/smooth_app/lib/pages/prices/price_location_card.dart b/packages/smooth_app/lib/pages/prices/price_location_card.dart index 8915880f379e..05d5e17eba64 100644 --- a/packages/smooth_app/lib/pages/prices/price_location_card.dart +++ b/packages/smooth_app/lib/pages/prices/price_location_card.dart @@ -8,6 +8,7 @@ import 'package:smooth_app/generic_lib/widgets/smooth_card.dart'; import 'package:smooth_app/pages/locations/osm_location.dart'; import 'package:smooth_app/pages/locations/search_location_helper.dart'; import 'package:smooth_app/pages/locations/search_location_preloaded_item.dart'; +import 'package:smooth_app/pages/prices/price_button.dart'; import 'package:smooth_app/pages/prices/price_model.dart'; import 'package:smooth_app/pages/search/search_page.dart'; @@ -15,9 +16,6 @@ import 'package:smooth_app/pages/search/search_page.dart'; class PriceLocationCard extends StatelessWidget { const PriceLocationCard(); - static const IconData _iconTodo = Icons.shopping_cart; - static const IconData _iconDone = Icons.place; - @override Widget build(BuildContext context) { final PriceModel model = context.watch(); @@ -33,7 +31,9 @@ class PriceLocationCard extends StatelessWidget { : location.getTitle() ?? location.getSubtitle() ?? location.getLatLng().toString(), - icon: location == null ? _iconTodo : _iconDone, + icon: location == null + ? Icons.shopping_cart + : PriceButton.locationIconData, onPressed: model.proof != null ? null : () async { diff --git a/packages/smooth_app/lib/pages/prices/price_location_widget.dart b/packages/smooth_app/lib/pages/prices/price_location_widget.dart new file mode 100644 index 000000000000..5ad7dd41a77e --- /dev/null +++ b/packages/smooth_app/lib/pages/prices/price_location_widget.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:openfoodfacts/openfoodfacts.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/prices/prices_page.dart'; +import 'package:smooth_app/query/product_query.dart'; + +/// Price Location display (no price data here). +class PriceLocationWidget extends StatelessWidget { + const PriceLocationWidget( + this.location, + ); + + final Location location; + + @override + Widget build(BuildContext context) { + final String? title = getLocationTitle(location); + return ListTile( + leading: const Icon(PriceButton.locationIconData), + title: title == null + ? null + : Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: location.displayName == null + ? null + : Text( + location.displayName!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ); + } + + static String? getLocationTitle(final Location? location) { + if (location == null) { + return null; + } + final StringBuffer result = StringBuffer(); + final String? countryEmoji = EmojiHelper.getCountryEmoji( + _getCountry(location), + ); + if (location.name != null) { + result.write(location.name); + } + if (location.city != null) { + if (result.isNotEmpty) { + result.write(', '); + } + result.write(location.city); + } + if (countryEmoji != null) { + if (result.isNotEmpty) { + result.write(' '); + } + result.write(countryEmoji); + } + if (result.isEmpty) { + return null; + } + return result.toString(); + } + + static OpenFoodFactsCountry? _getCountry(final Location location) => + OpenFoodFactsCountry.fromOffTag(location.countryCode); + + static Future showLocationPrices({ + required final int locationId, + required final BuildContext context, + }) async => + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => PricesPage( + GetPricesModel( + parameters: GetPricesModel.getStandardPricesParameters() + ..locationId = locationId, + displayEachLocation: false, + uri: OpenPricesAPIClient.getUri( + path: 'locations/$locationId', + uriHelper: ProductQuery.uriPricesHelper, + ), + title: AppLocalizations.of(context) + .all_search_prices_top_location_single_title, + ), + ), + ), + ); +} diff --git a/packages/smooth_app/lib/pages/prices/price_product_widget.dart b/packages/smooth_app/lib/pages/prices/price_product_widget.dart index ef898f5ccd44..a3108578b1ef 100644 --- a/packages/smooth_app/lib/pages/prices/price_product_widget.dart +++ b/packages/smooth_app/lib/pages/prices/price_product_widget.dart @@ -2,11 +2,16 @@ import 'package:auto_size_text/auto_size_text.dart'; 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/database/dao_product.dart'; +import 'package:smooth_app/database/local_database.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/get_prices_model.dart'; import 'package:smooth_app/pages/prices/price_button.dart'; import 'package:smooth_app/pages/prices/price_count_widget.dart'; +import 'package:smooth_app/pages/prices/price_meta_product.dart'; +import 'package:smooth_app/pages/prices/prices_page.dart'; /// Price Product display (no price data here). class PriceProductWidget extends StatelessWidget { @@ -71,9 +76,32 @@ class PriceProductWidget extends StatelessWidget { runSpacing: 0, children: [ PriceCountWidget( - priceCount, - priceProduct: priceProduct, - enableCountButton: model.enableCountButton, + count: priceCount, + onPressed: !model.enableCountButton + ? null + : () async { + final LocalDatabase localDatabase = + context.read(); + final Product? newProduct = + await DaoProduct(localDatabase) + .get(priceProduct.code); + if (!context.mounted) { + return; + } + return Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => PricesPage( + GetPricesModel.product( + product: newProduct != null + ? PriceMetaProduct.product(newProduct) + : PriceMetaProduct.priceProduct( + priceProduct), + context: context, + ), + ), + ), + ); + }, ), if (brands != null) for (final String brand in brands) @@ -85,7 +113,7 @@ class PriceProductWidget extends StatelessWidget { if (unknown) PriceButton( title: appLocalizations.prices_unknown_product, - iconData: Icons.warning, + iconData: PriceButton.warningIconData, onPressed: null, buttonStyle: ElevatedButton.styleFrom( disabledForegroundColor: Colors.red, diff --git a/packages/smooth_app/lib/pages/prices/price_user_button.dart b/packages/smooth_app/lib/pages/prices/price_user_button.dart index 02f1a9ce4149..bffc3ff89d80 100644 --- a/packages/smooth_app/lib/pages/prices/price_user_button.dart +++ b/packages/smooth_app/lib/pages/prices/price_user_button.dart @@ -29,18 +29,9 @@ class PriceUserButton extends StatelessWidget { MaterialPageRoute( builder: (BuildContext context) => PricesPage( GetPricesModel( - parameters: GetPricesParameters() - ..owner = user - ..orderBy = >[ - const OrderBy( - field: GetPricesOrderField.created, - ascending: false, - ), - ] - ..pageSize = GetPricesModel.pageSize - ..pageNumber = 1, - displayOwner: false, - displayProduct: true, + parameters: GetPricesModel.getStandardPricesParameters() + ..owner = user, + displayEachOwner: false, uri: OpenPricesAPIClient.getUri( path: 'users/$user', uriHelper: ProductQuery.uriPricesHelper, @@ -57,7 +48,7 @@ class PriceUserButton extends StatelessWidget { Widget build(BuildContext context) => PriceButton( tooltip: AppLocalizations.of(context).prices_open_user_proofs(user), title: user, - iconData: Icons.account_box, + iconData: PriceButton.userIconData, onPressed: () async => showUserPrices( user: user, context: context, diff --git a/packages/smooth_app/lib/pages/prices/prices_locations_page.dart b/packages/smooth_app/lib/pages/prices/prices_locations_page.dart new file mode 100644 index 000000000000..f941b6e25253 --- /dev/null +++ b/packages/smooth_app/lib/pages/prices/prices_locations_page.dart @@ -0,0 +1,169 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:matomo_tracker/matomo_tracker.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; +import 'package:smooth_app/generic_lib/design_constants.dart'; +import 'package:smooth_app/generic_lib/widgets/smooth_back_button.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/price_button.dart'; +import 'package:smooth_app/pages/prices/price_count_widget.dart'; +import 'package:smooth_app/pages/prices/price_location_widget.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 top prices locations. +class PricesLocationsPage extends StatefulWidget { + const PricesLocationsPage(); + + @override + State createState() => _PricesLocationsPageState(); +} + +class _PricesLocationsPageState extends State + with TraceableClientMixin { + late final Future> _locations = + _showTopLocations(); + + // In this specific page, let's never try to go beyond the top 10. + // cf. https://github.com/openfoodfacts/smooth-app/pull/5383#issuecomment-2171117141 + static const int _pageSize = 10; + + @override + Widget build(BuildContext context) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + return SmoothScaffold( + appBar: SmoothAppBar( + centerTitle: false, + leading: const SmoothBackButton(), + title: Text( + appLocalizations.all_search_prices_top_location_title, + ), + actions: [ + IconButton( + tooltip: appLocalizations.prices_app_button, + icon: const Icon(Icons.open_in_new), + onPressed: () async => LaunchUrlHelper.launchURL( + OpenPricesAPIClient.getUri( + path: 'locations', + uriHelper: ProductQuery.uriPricesHelper, + ).toString(), + ), + ), + ], + ), + body: FutureBuilder>( + future: _locations, + builder: ( + final BuildContext context, + final AsyncSnapshot> snapshot, + ) { + if (snapshot.connectionState != ConnectionState.done) { + return const CircularProgressIndicator(); + } + if (snapshot.hasError) { + return Text(snapshot.error!.toString()); + } + // highly improbable + if (!snapshot.hasData) { + return const Text('no data'); + } + if (snapshot.data!.isError) { + return Text(snapshot.data!.error!); + } + final GetLocationsResult result = snapshot.data!.value; + // highly improbable + if (result.items == null) { + return const Text('empty list'); + } + final List children = []; + final AppLocalizations appLocalizations = + AppLocalizations.of(context); + + for (final Location item in result.items!) { + final int priceCount = item.priceCount ?? 0; + children.add( + SmoothCard( + child: Wrap( + spacing: VERY_SMALL_SPACE, + children: [ + PriceLocationWidget(item), + PriceCountWidget( + count: priceCount, + onPressed: () async => + PriceLocationWidget.showLocationPrices( + locationId: item.locationId, + context: context, + ), + ), + PriceButton( + onPressed: () {}, + title: '${item.userCount}', + iconData: PriceButton.userIconData, + tooltip: item.userCount == null + ? null + : appLocalizations.prices_button_count_user( + item.userCount!, + ), + ), + PriceButton( + onPressed: () {}, + title: '${item.productCount}', + iconData: PriceButton.productIconData, + tooltip: item.productCount == null + ? null + : appLocalizations.prices_button_count_product( + item.productCount!, + ), + ), + PriceButton( + onPressed: () {}, + title: '${item.proofCount}', + iconData: PriceButton.proofIconData, + tooltip: item.proofCount == null + ? null + : appLocalizations.prices_button_count_proof( + item.proofCount!, + ), + ), + ], + ), + ), + ); + } + final String title = + appLocalizations.prices_locations_list_length_many_pages( + _pageSize, + result.total!, + ); + children.insert( + 0, + SmoothCard(child: ListTile(title: Text(title))), + ); + // so that the last content gets not hidden by the FAB + children.add( + const SizedBox(height: 2 * MINIMUM_TOUCH_SIZE), + ); + return ListView( + children: children, + ); + }, + ), + ); + } + + static Future> _showTopLocations() async => + OpenPricesAPIClient.getLocations( + GetLocationsParameters() + ..orderBy = >[ + const OrderBy( + field: GetLocationsOrderField.priceCount, + ascending: false, + ), + ] + ..pageSize = _pageSize + ..pageNumber = 1, + uriHelper: ProductQuery.uriPricesHelper, + ); +} diff --git a/packages/smooth_app/lib/pages/prices/prices_users_page.dart b/packages/smooth_app/lib/pages/prices/prices_users_page.dart index f430bae6d40f..3a12aa1143c0 100644 --- a/packages/smooth_app/lib/pages/prices/prices_users_page.dart +++ b/packages/smooth_app/lib/pages/prices/prices_users_page.dart @@ -6,7 +6,6 @@ import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_back_button.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/price_button.dart'; import 'package:smooth_app/pages/prices/price_count_widget.dart'; import 'package:smooth_app/pages/prices/price_user_button.dart'; import 'package:smooth_app/query/product_query.dart'; @@ -86,21 +85,12 @@ class _PricesUsersPageState extends State spacing: VERY_SMALL_SPACE, children: [ PriceUserButton(item.userId), - PriceButton( + PriceCountWidget( + count: priceCount, onPressed: () async => PriceUserButton.showUserPrices( user: item.userId, context: context, ), - iconData: Icons.label, - title: '$priceCount', - buttonStyle: ElevatedButton.styleFrom( - foregroundColor: PriceCountWidget.getForegroundColor( - priceCount, - ), - backgroundColor: PriceCountWidget.getBackgroundColor( - priceCount, - ), - ), ), ], ), 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 4bfe892cc683..26bf878a88d9 100644 --- a/packages/smooth_app/lib/pages/prices/product_prices_list.dart +++ b/packages/smooth_app/lib/pages/prices/product_prices_list.dart @@ -10,6 +10,7 @@ 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/get_prices_model.dart'; import 'package:smooth_app/pages/prices/price_data_widget.dart'; +import 'package:smooth_app/pages/prices/price_location_widget.dart'; import 'package:smooth_app/pages/prices/price_product_widget.dart'; import 'package:smooth_app/query/product_query.dart'; @@ -70,7 +71,7 @@ class _ProductPricesListState extends State } final List children = []; - if (!widget.model.displayProduct) { + if (!widget.model.displayEachProduct) { // in that case we display the product only once, if possible. for (final Price price in result.items!) { final PriceProduct? priceProduct = price.product; @@ -88,6 +89,21 @@ class _ProductPricesListState extends State break; } } + if (!widget.model.displayEachLocation) { + // in that case we display the location only once, if possible. + for (final Price price in result.items!) { + final Location? location = price.location; + if (location == null) { + continue; + } + children.add( + SmoothCard( + child: PriceLocationWidget(location), + ), + ); + break; + } + } for (final Price price in result.items!) { final PriceProduct? priceProduct = price.product; @@ -97,7 +113,7 @@ class _ProductPricesListState extends State mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (widget.model.displayProduct && priceProduct != null) + if (widget.model.displayEachProduct && priceProduct != null) PriceProductWidget( priceProduct, model: widget.model,