diff --git a/packages/smooth_app/lib/background/background_task_download_products.dart b/packages/smooth_app/lib/background/background_task_download_products.dart index 4366496be90..79c2171585d 100644 --- a/packages/smooth_app/lib/background/background_task_download_products.dart +++ b/packages/smooth_app/lib/background/background_task_download_products.dart @@ -112,6 +112,7 @@ class BackgroundTaskDownloadProducts extends BackgroundTaskProgressing { if (downloadFlag & flagMaskExcludeKP != 0) { fields.remove(ProductField.KNOWLEDGE_PANELS); } + final OpenFoodFactsLanguage language = ProductQuery.getLanguage(); final SearchResult searchResult = await OpenFoodAPIClient.searchProducts( getUser(), ProductSearchQueryConfiguration( @@ -121,7 +122,7 @@ class BackgroundTaskDownloadProducts extends BackgroundTaskProgressing { const PageNumber(page: 1), BarcodeParameter.list(barcodes), ], - language: ProductQuery.getLanguage(), + language: language, country: ProductQuery.getCountry(), version: ProductQuery.productQueryVersion, ), @@ -134,7 +135,7 @@ class BackgroundTaskDownloadProducts extends BackgroundTaskProgressing { final DaoProduct daoProduct = DaoProduct(localDatabase); for (final Product product in downloadedProducts) { if (await _shouldBeUpdated(daoProduct, product.barcode!)) { - await daoProduct.put(product); + await daoProduct.put(product, language); } } final int deleted = await daoWorkBarcode.deleteBarcodes(work, barcodes); diff --git a/packages/smooth_app/lib/cards/category_cards/svg_safe_network.dart b/packages/smooth_app/lib/cards/category_cards/svg_safe_network.dart index 31e5341d378..3a534cb4023 100644 --- a/packages/smooth_app/lib/cards/category_cards/svg_safe_network.dart +++ b/packages/smooth_app/lib/cards/category_cards/svg_safe_network.dart @@ -26,18 +26,69 @@ class _SvgSafeNetworkState extends State { late final Future _loading = _load(); String get _url => widget.helper.url; - +// TODO(monsieurtanuki): Change /dist/ url to be the first try when the majority of products have been updated + /// Loads the SVG file from url or from alternate url. + /// + /// In Autumn 2023, the web image folders were moved to a /dist/ subfolder. + /// Before: + /// https://static.openfoodfacts.org/images/attributes/nova-group-3.svg + /// After: + /// https://static.openfoodfacts.org/images/attributes/dist/nova-group-3.svg + /// Products that haven't been refreshed still reference the previous web + /// folder. If we cannot find the URL, we try with the alternate /dist/ URL. Future _load() async { + const int statusOk = 200; + const int statusNotFound = 404; + + final String? alternateUrl = _getAlternateUrl(); + + // is the url already cached? String? cached = _networkCache[_url]; if (cached != null) { return cached; } - final http.Response response = await http.get(Uri.parse(_url)); - if (response.statusCode != 200) { - throw Exception('Failed to load SVG: $_url ${response.statusCode}'); + // is the alternate url already cached? + if (alternateUrl != null) { + cached = _networkCache[alternateUrl]; + if (cached != null) { + return cached; + } + } + + // try with the url + final http.Response response1 = await http.get(Uri.parse(_url)); + if (response1.statusCode == statusOk) { + _networkCache[_url] = cached = response1.body; + return cached; + } + if (response1.statusCode == statusNotFound && alternateUrl != null) { + // try with the alternate url + final http.Response response2 = await http.get(Uri.parse(alternateUrl)); + if (response2.statusCode == statusOk) { + _networkCache[alternateUrl] = cached = response2.body; + return cached; + } + } + + throw Exception('Failed to load SVG: $_url ${response1.statusCode}'); + } + + /// Returns the alternate /dist/ url or null if irrelevant. + String? _getAlternateUrl() { + const String lastPathSegment = '/dist/'; + if (_url.contains(lastPathSegment)) { + return null; + } + final int lastSlashPos = _url.lastIndexOf('/'); + if (lastSlashPos == -1 || + lastSlashPos == 0 || + lastSlashPos == _url.length - 1) { + // very unlikely + return null; } - _networkCache[_url] = cached = response.body; - return cached; + return '${_url.substring(0, lastSlashPos)}' + '$lastPathSegment' + '${_url.substring(lastSlashPos + 1)}'; } @override diff --git a/packages/smooth_app/lib/data_models/onboarding_data_product.dart b/packages/smooth_app/lib/data_models/onboarding_data_product.dart index 417be1025a9..4deb889c166 100644 --- a/packages/smooth_app/lib/data_models/onboarding_data_product.dart +++ b/packages/smooth_app/lib/data_models/onboarding_data_product.dart @@ -38,6 +38,7 @@ class OnboardingDataProduct extends AbstractOnboardingData { OpenFoodAPIClient.getProductString( ProductRefresher().getBarcodeQueryConfiguration( AbstractOnboardingData.barcode, + ProductQuery.getLanguage(), ), uriHelper: ProductQuery.uriProductHelper, ).timeout(SnackBarDuration.long); diff --git a/packages/smooth_app/lib/data_models/query_product_list_supplier.dart b/packages/smooth_app/lib/data_models/query_product_list_supplier.dart index e762a032f88..8d624b2058e 100644 --- a/packages/smooth_app/lib/data_models/query_product_list_supplier.dart +++ b/packages/smooth_app/lib/data_models/query_product_list_supplier.dart @@ -23,7 +23,10 @@ class QueryProductListSupplier extends ProductListSupplier { productList.setAll(searchResult.products!); productList.totalSize = searchResult.count ?? 0; partialProductList.add(productList); - await DaoProduct(localDatabase).putAll(searchResult.products!); + await DaoProduct(localDatabase).putAll( + searchResult.products!, + productQuery.language, + ); } await DaoProductList(localDatabase).put(productList); return null; diff --git a/packages/smooth_app/lib/database/dao_hive_product.dart b/packages/smooth_app/lib/database/dao_hive_product.dart index 8f29bfc0757..40aee8a6515 100644 --- a/packages/smooth_app/lib/database/dao_hive_product.dart +++ b/packages/smooth_app/lib/database/dao_hive_product.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'package:hive/hive.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:smooth_app/database/abstract_dao.dart'; -import 'package:smooth_app/database/dao_product_migration.dart'; import 'package:smooth_app/database/local_database.dart'; /// Hive type adapter for [Product] @@ -24,7 +23,7 @@ class _ProductAdapter extends TypeAdapter { /// But Hive needs it - it doesn't like data to be removed... /// Where we store the products as "barcode => product". @Deprecated('use [DaoProduct] instead') -class DaoHiveProduct extends AbstractDao implements DaoProductMigrationSource { +class DaoHiveProduct extends AbstractDao { @Deprecated('use [DaoProduct] instead') DaoHiveProduct(final LocalDatabase localDatabase) : super(localDatabase); @@ -35,48 +34,4 @@ class DaoHiveProduct extends AbstractDao implements DaoProductMigrationSource { @override void registerAdapter() => Hive.registerAdapter(_ProductAdapter()); - - LazyBox _getBox() => Hive.lazyBox(_hiveBoxName); - - Future get(final String barcode) async => _getBox().get(barcode); - - @override - Future> getAll(final List barcodes) async { - final LazyBox box = _getBox(); - final Map result = {}; - for (final String barcode in barcodes) { - final Product? product = await box.get(barcode); - if (product != null) { - result[barcode] = product; - } - } - return result; - } - - Future put(final Product product) async => putAll([product]); - - Future putAll(final Iterable products) async { - final Map upserts = {}; - for (final Product product in products) { - upserts[product.barcode!] = product; - } - await _getBox().putAll(upserts); - } - - @override - Future> getAllKeys() async { - final LazyBox box = _getBox(); - final List result = []; - for (final dynamic key in box.keys) { - result.add(key.toString()); - } - return result; - } - - // Just for the migration - @override - Future deleteAll(final List barcodes) async { - final LazyBox box = _getBox(); - await box.deleteAll(barcodes); - } } diff --git a/packages/smooth_app/lib/database/dao_product.dart b/packages/smooth_app/lib/database/dao_product.dart index 8b34ef144f0..b346f05d4f5 100644 --- a/packages/smooth_app/lib/database/dao_product.dart +++ b/packages/smooth_app/lib/database/dao_product.dart @@ -8,12 +8,10 @@ import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:smooth_app/database/abstract_sql_dao.dart'; import 'package:smooth_app/database/bulk_deletable.dart'; import 'package:smooth_app/database/bulk_manager.dart'; -import 'package:smooth_app/database/dao_product_migration.dart'; import 'package:smooth_app/database/local_database.dart'; import 'package:sqflite/sqflite.dart'; -class DaoProduct extends AbstractSqlDao - implements BulkDeletable, DaoProductMigrationDestination { +class DaoProduct extends AbstractSqlDao implements BulkDeletable { DaoProduct(super.localDatabase); static const String _TABLE_PRODUCT = 'gzipped_product'; @@ -21,11 +19,13 @@ class DaoProduct extends AbstractSqlDao static const String _TABLE_PRODUCT_COLUMN_GZIPPED_JSON = 'encoded_gzipped_json'; static const String _TABLE_PRODUCT_COLUMN_LAST_UPDATE = 'last_update'; + static const String _TABLE_PRODUCT_COLUMN_LANGUAGE = 'lc'; static const List _columns = [ _TABLE_PRODUCT_COLUMN_BARCODE, _TABLE_PRODUCT_COLUMN_GZIPPED_JSON, _TABLE_PRODUCT_COLUMN_LAST_UPDATE, + _TABLE_PRODUCT_COLUMN_LANGUAGE, ]; static FutureOr onUpgrade( @@ -41,6 +41,10 @@ class DaoProduct extends AbstractSqlDao ',$_TABLE_PRODUCT_COLUMN_LAST_UPDATE INT NOT NULL' ')'); } + if (oldVersion < 4) { + await db.execute('alter table $_TABLE_PRODUCT add column ' + '$_TABLE_PRODUCT_COLUMN_LANGUAGE TEXT'); + } } /// Returns the [Product] that matches the [barcode], or null. @@ -87,17 +91,28 @@ class DaoProduct extends AbstractSqlDao return result; } - Future put(final Product product) async => putAll([product]); + Future put( + final Product product, + final OpenFoodFactsLanguage language, + ) async => + putAll( + [product], + language, + ); /// Replaces products in database - @override - Future putAll(final Iterable products) async => + Future putAll( + final Iterable products, + final OpenFoodFactsLanguage language, + ) async => localDatabase.database.transaction( - (final Transaction transaction) async => - _bulkReplaceLoop(transaction, products), + (final Transaction transaction) async => _bulkReplaceLoop( + transaction, + products, + language, + ), ); - @override Future> getAllKeys() async { final List result = []; final List> queryResults = @@ -126,6 +141,7 @@ class DaoProduct extends AbstractSqlDao Future _bulkReplaceLoop( final DatabaseExecutor databaseExecutor, final Iterable products, + final OpenFoodFactsLanguage language, ) async { final int lastUpdate = LocalDatabase.nowInMillis(); final BulkManager bulkManager = BulkManager(); @@ -138,6 +154,7 @@ class DaoProduct extends AbstractSqlDao ), ); insertParameters.add(lastUpdate); + insertParameters.add(language.offTag); } await bulkManager.insert( bulkInsertable: this, @@ -223,6 +240,7 @@ class DaoProduct extends AbstractSqlDao await localDatabase.database.rawQuery(''' select sum(length($_TABLE_PRODUCT_COLUMN_BARCODE)) + sum(length($_TABLE_PRODUCT_COLUMN_LAST_UPDATE)) + + sum(length($_TABLE_PRODUCT_COLUMN_LANGUAGE)) + sum(length($_TABLE_PRODUCT_COLUMN_GZIPPED_JSON)) from $_TABLE_PRODUCT '''), diff --git a/packages/smooth_app/lib/database/dao_product_migration.dart b/packages/smooth_app/lib/database/dao_product_migration.dart deleted file mode 100644 index cc504fca682..00000000000 --- a/packages/smooth_app/lib/database/dao_product_migration.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:openfoodfacts/openfoodfacts.dart'; - -/// Helper around product data migration. -abstract class DaoProductMigration { - Future> getAllKeys(); - - /// Migrates product data from a [source] to a [destination]. - /// - /// Will empty the source in the end if successful. - static Future migrate({ - required final DaoProductMigrationSource source, - required final DaoProductMigrationDestination destination, - }) async { - final List barcodesFrom = await source.getAllKeys(); - if (barcodesFrom.isEmpty) { - // nothing to migrate, or already migrated and cleaned. - return; - } - - final List barcodesAlreadyThere = await destination.getAllKeys(); - - final List barcodesToBeCopied = List.from(barcodesFrom); - barcodesToBeCopied.removeWhere( - (final String barcode) => barcodesAlreadyThere.contains(barcode)); - - if (barcodesToBeCopied.isNotEmpty) { - final Map copiedProducts = - await source.getAll(barcodesToBeCopied); - await destination.putAll(copiedProducts.values); - final List barcodesFinallyThere = await destination.getAllKeys(); - if (barcodesFinallyThere.length != - barcodesAlreadyThere.length + barcodesToBeCopied.length) { - throw Exception('Unexpected difference between counts'); - } - } - - // cleaning the old product table - await source.deleteAll(barcodesFrom); - final List barcodesNoMore = await source.getAllKeys(); - if (barcodesNoMore.isNotEmpty) { - throw Exception('Unexpected not empty source'); - } - } -} - -/// Source of a dao product migration. -abstract class DaoProductMigrationSource implements DaoProductMigration { - Future> getAll(final List barcodes); - Future deleteAll(final List barcodes); -} - -/// Destination of a dao product migration. -abstract class DaoProductMigrationDestination implements DaoProductMigration { - Future putAll(final Iterable products); -} diff --git a/packages/smooth_app/lib/database/local_database.dart b/packages/smooth_app/lib/database/local_database.dart index f6a2d4336cf..a1a282929fd 100644 --- a/packages/smooth_app/lib/database/local_database.dart +++ b/packages/smooth_app/lib/database/local_database.dart @@ -62,7 +62,7 @@ class LocalDatabase extends ChangeNotifier { final String databasePath = join(databasesRootPath, 'smoothie.db'); final Database database = await openDatabase( databasePath, - version: 3, + version: 4, singleInstance: true, onUpgrade: _onUpgrade, ); diff --git a/packages/smooth_app/lib/helpers/product_cards_helper.dart b/packages/smooth_app/lib/helpers/product_cards_helper.dart index 69d5197f5ce..f1eef8c282e 100644 --- a/packages/smooth_app/lib/helpers/product_cards_helper.dart +++ b/packages/smooth_app/lib/helpers/product_cards_helper.dart @@ -10,15 +10,26 @@ import 'package:smooth_app/generic_lib/widgets/smooth_card.dart'; import 'package:smooth_app/helpers/image_field_extension.dart'; import 'package:smooth_app/helpers/ui_helpers.dart'; import 'package:smooth_app/query/product_query.dart'; +import 'package:smooth_app/widgets/smooth_app_bar.dart'; -Widget buildProductTitle( - final Product product, - final AppLocalizations appLocalizations, -) => - Text( - getProductNameAndBrands(product, appLocalizations), - overflow: TextOverflow.ellipsis, - maxLines: 1, +SmoothAppBar buildEditProductAppBar({ + required final BuildContext context, + required final String title, + required final Product product, +}) => + SmoothAppBar( + centerTitle: false, + title: Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subTitle: Text( + getProductNameAndBrands(product, AppLocalizations.of(context)), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ignoreSemanticsForSubtitle: true, ); String getProductNameAndBrands( diff --git a/packages/smooth_app/lib/pages/image/product_image_other_page.dart b/packages/smooth_app/lib/pages/image/product_image_other_page.dart index 0bce9f87d33..6f985e81457 100644 --- a/packages/smooth_app/lib/pages/image/product_image_other_page.dart +++ b/packages/smooth_app/lib/pages/image/product_image_other_page.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:smooth_app/helpers/product_cards_helper.dart'; -import 'package:smooth_app/widgets/smooth_app_bar.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; /// Full page display of a raw product image. @@ -19,10 +18,10 @@ class ProductImageOtherPage extends StatelessWidget { Widget build(BuildContext context) { final AppLocalizations appLocalizations = AppLocalizations.of(context); return SmoothScaffold( - appBar: SmoothAppBar( - centerTitle: false, - title: Text(appLocalizations.edit_product_form_item_photos_title), - subTitle: buildProductTitle(product, appLocalizations), + appBar: buildEditProductAppBar( + context: context, + title: appLocalizations.edit_product_form_item_photos_title, + product: product, ), body: Image( image: NetworkImage( diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_language_selector.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_language_selector.dart index 98bb9fc9959..914918eae52 100644 --- a/packages/smooth_app/lib/pages/preferences/user_preferences_language_selector.dart +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_language_selector.dart @@ -3,6 +3,7 @@ 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/preferences/user_preferences.dart'; +import 'package:smooth_app/data_models/product_preferences.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/widgets/language_selector.dart'; import 'package:smooth_app/pages/preferences/user_preferences_item.dart'; @@ -23,6 +24,19 @@ class UserPreferencesLanguageSelector extends StatelessWidget { ); } + Future _changeAppLanguage( + BuildContext context, + UserPreferences userPreferences, { + required OpenFoodFactsLanguage language, + }) async { + ProductQuery.setLanguage( + context, + userPreferences, + languageCode: language.code, + ); + await context.read().refresh(); + } + @override Widget build(BuildContext context) { final AppLocalizations appLocalizations = AppLocalizations.of(context); @@ -39,13 +53,15 @@ class UserPreferencesLanguageSelector extends StatelessWidget { ), child: LanguageSelector( setLanguage: (final OpenFoodFactsLanguage? language) async { - if (language != null) { - ProductQuery.setLanguage( - context, - userPreferences, - languageCode: language.code, - ); + if (language == null) { + return; } + + _changeAppLanguage( + context, + userPreferences, + language: language, + ); }, selectedLanguages: [ ProductQuery.getLanguage(), diff --git a/packages/smooth_app/lib/pages/product/add_basic_details_page.dart b/packages/smooth_app/lib/pages/product/add_basic_details_page.dart index e017feeb115..14b68bc3efc 100644 --- a/packages/smooth_app/lib/pages/product/add_basic_details_page.dart +++ b/packages/smooth_app/lib/pages/product/add_basic_details_page.dart @@ -15,7 +15,6 @@ import 'package:smooth_app/pages/product/common/product_refresher.dart'; import 'package:smooth_app/pages/product/may_exit_page_helper.dart'; import 'package:smooth_app/pages/product/multilingual_helper.dart'; import 'package:smooth_app/pages/text_field_helper.dart'; -import 'package:smooth_app/widgets/smooth_app_bar.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; import 'package:smooth_app/widgets/will_pop_scope.dart'; @@ -92,10 +91,10 @@ class _AddBasicDetailsPageState extends State { child: UnfocusWhenTapOutside( child: SmoothScaffold( fixKeyboard: true, - appBar: SmoothAppBar( - centerTitle: false, - title: Text(appLocalizations.basic_details), - subTitle: buildProductTitle(widget.product, appLocalizations), + appBar: buildEditProductAppBar( + context: context, + title: appLocalizations.basic_details, + product: widget.product, ), body: Form( key: _formKey, diff --git a/packages/smooth_app/lib/pages/product/add_other_details_page.dart b/packages/smooth_app/lib/pages/product/add_other_details_page.dart index 7274bff0347..93d2d5e68e9 100644 --- a/packages/smooth_app/lib/pages/product/add_other_details_page.dart +++ b/packages/smooth_app/lib/pages/product/add_other_details_page.dart @@ -9,7 +9,6 @@ import 'package:smooth_app/helpers/product_cards_helper.dart'; import 'package:smooth_app/pages/product/common/product_buttons.dart'; import 'package:smooth_app/pages/product/may_exit_page_helper.dart'; import 'package:smooth_app/pages/text_field_helper.dart'; -import 'package:smooth_app/widgets/smooth_app_bar.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; import 'package:smooth_app/widgets/will_pop_scope.dart'; @@ -59,12 +58,10 @@ class _AddOtherDetailsPageState extends State { onWillPop: () async => (await _mayExitPage(saving: false), null), child: SmoothScaffold( fixKeyboard: true, - appBar: SmoothAppBar( - centerTitle: false, - title: - Text(appLocalizations.edit_product_form_item_other_details_title), - subTitle: buildProductTitle(widget.product, appLocalizations), - ignoreSemanticsForSubtitle: true, + appBar: buildEditProductAppBar( + context: context, + title: appLocalizations.edit_product_form_item_other_details_title, + product: widget.product, ), body: Form( key: _formKey, @@ -76,15 +73,6 @@ class _AddOtherDetailsPageState extends State { padding: EdgeInsets.symmetric(horizontal: size.width * 0.05), child: Column( children: [ - ExcludeSemantics( - child: Text( - appLocalizations.barcode_barcode(_product.barcode!), - style: - Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ), SizedBox(height: _heightSpace), SmoothTextFormField( controller: _websiteController, diff --git a/packages/smooth_app/lib/pages/product/common/product_list_item_simple.dart b/packages/smooth_app/lib/pages/product/common/product_list_item_simple.dart index 31b462949ae..efd699d6ed4 100644 --- a/packages/smooth_app/lib/pages/product/common/product_list_item_simple.dart +++ b/packages/smooth_app/lib/pages/product/common/product_list_item_simple.dart @@ -39,12 +39,6 @@ class _ProductListItemSimpleState extends State { _model = ProductModel(widget.barcode, localDatabase); } - @override - void dispose() { - _model.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) => ChangeNotifierProvider( create: (final BuildContext context) => _model, diff --git a/packages/smooth_app/lib/pages/product/common/product_list_page.dart b/packages/smooth_app/lib/pages/product/common/product_list_page.dart index 2c77e01c418..f90d1d00e1c 100644 --- a/packages/smooth_app/lib/pages/product/common/product_list_page.dart +++ b/packages/smooth_app/lib/pages/product/common/product_list_page.dart @@ -479,16 +479,20 @@ class _ProductListPageState extends State final LocalDatabase localDatabase, ) async { try { + final OpenFoodFactsLanguage language = ProductQuery.getLanguage(); final SearchResult searchResult = await OpenFoodAPIClient.searchProducts( ProductQuery.getUser(), - ProductRefresher().getBarcodeListQueryConfiguration(barcodes), + ProductRefresher().getBarcodeListQueryConfiguration( + barcodes, + language, + ), uriHelper: ProductQuery.uriProductHelper, ); final List? freshProducts = searchResult.products; if (freshProducts == null) { return false; } - await DaoProduct(localDatabase).putAll(freshProducts); + await DaoProduct(localDatabase).putAll(freshProducts, language); localDatabase.upToDate.setLatestDownloadedProducts(freshProducts); final RobotoffInsightHelper robotoffInsightHelper = RobotoffInsightHelper(localDatabase); diff --git a/packages/smooth_app/lib/pages/product/common/product_refresher.dart b/packages/smooth_app/lib/pages/product/common/product_refresher.dart index 5b5a815cce6..f9d2a8f2e99 100644 --- a/packages/smooth_app/lib/pages/product/common/product_refresher.dart +++ b/packages/smooth_app/lib/pages/product/common/product_refresher.dart @@ -74,23 +74,25 @@ class ProductRefresher { /// Returns the standard configuration for barcode product query. ProductQueryConfiguration getBarcodeQueryConfiguration( final String barcode, + final OpenFoodFactsLanguage language, ) => ProductQueryConfiguration( barcode, fields: ProductQuery.fields, - language: ProductQuery.getLanguage(), + language: language, country: ProductQuery.getCountry(), version: ProductQuery.productQueryVersion, ); /// Returns the standard configuration for several barcodes product query. ProductSearchQueryConfiguration getBarcodeListQueryConfiguration( - final List barcodes, { + final List barcodes, + final OpenFoodFactsLanguage language, { final List? fields, }) => ProductSearchQueryConfiguration( fields: fields ?? ProductQuery.fields, - language: ProductQuery.getLanguage(), + language: language, country: ProductQuery.getCountry(), parametersList: [ BarcodeParameter.list(barcodes), @@ -159,12 +161,16 @@ class ProductRefresher { required final String barcode, }) async { try { + final OpenFoodFactsLanguage language = ProductQuery.getLanguage(); final ProductResultV3 result = await OpenFoodAPIClient.getProductV3( - getBarcodeQueryConfiguration(barcode), + getBarcodeQueryConfiguration( + barcode, + language, + ), uriHelper: ProductQuery.uriProductHelper, ); if (result.product != null) { - await DaoProduct(localDatabase).put(result.product!); + await DaoProduct(localDatabase).put(result.product!, language); localDatabase.upToDate.setLatestDownloadedProduct(result.product!); localDatabase.notifyListeners(); return FetchedProduct.found(result.product!); @@ -198,15 +204,16 @@ class ProductRefresher { final List barcodes, ) async { try { + final OpenFoodFactsLanguage language = ProductQuery.getLanguage(); final SearchResult searchResult = await OpenFoodAPIClient.searchProducts( ProductQuery.getUser(), - getBarcodeListQueryConfiguration(barcodes), + getBarcodeListQueryConfiguration(barcodes, language), uriHelper: ProductQuery.uriProductHelper, ); if (searchResult.products == null) { return null; } - await DaoProduct(localDatabase).putAll(searchResult.products!); + await DaoProduct(localDatabase).putAll(searchResult.products!, language); localDatabase.upToDate .setLatestDownloadedProducts(searchResult.products!); localDatabase.notifyListeners(); diff --git a/packages/smooth_app/lib/pages/product/edit_image_button.dart b/packages/smooth_app/lib/pages/product/edit_image_button.dart index bcd2d80b2af..e7ab6df11e3 100644 --- a/packages/smooth_app/lib/pages/product/edit_image_button.dart +++ b/packages/smooth_app/lib/pages/product/edit_image_button.dart @@ -1,3 +1,4 @@ +import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; @@ -18,28 +19,35 @@ class EditImageButton extends StatelessWidget { @override Widget build(BuildContext context) { final ColorScheme colorScheme = Theme.of(context).colorScheme; - return OutlinedButton.icon( - icon: Icon(iconData), - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(colorScheme.onPrimary), - shape: MaterialStateProperty.all( - const RoundedRectangleBorder(borderRadius: ROUNDED_BORDER_RADIUS), - ), - side: borderWidth == null - ? null - : MaterialStateBorderSide.resolveWith( - (_) => BorderSide( - color: colorScheme.primary, - width: borderWidth!, + return Tooltip( + message: label, + child: OutlinedButton.icon( + icon: Icon(iconData), + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(colorScheme.onPrimary), + shape: MaterialStateProperty.all( + const RoundedRectangleBorder(borderRadius: ROUNDED_BORDER_RADIUS), + ), + side: borderWidth == null + ? null + : MaterialStateBorderSide.resolveWith( + (_) => BorderSide( + color: colorScheme.primary, + width: borderWidth!, + ), ), - ), - ), - onPressed: onPressed, - label: SizedBox( - width: double.infinity, - child: Padding( - padding: EdgeInsets.all(borderWidth ?? 0), - child: Text(label), + ), + onPressed: onPressed, + label: SizedBox( + width: double.infinity, + child: Padding( + padding: EdgeInsets.all(borderWidth ?? 0), + child: AutoSizeText( + label, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), ), ), ); diff --git a/packages/smooth_app/lib/pages/product/edit_new_packagings.dart b/packages/smooth_app/lib/pages/product/edit_new_packagings.dart index f3da4ab5eff..f3ec7598c87 100644 --- a/packages/smooth_app/lib/pages/product/edit_new_packagings.dart +++ b/packages/smooth_app/lib/pages/product/edit_new_packagings.dart @@ -20,7 +20,6 @@ import 'package:smooth_app/pages/product/may_exit_page_helper.dart'; import 'package:smooth_app/pages/product/simple_input_number_field.dart'; import 'package:smooth_app/query/product_query.dart'; import 'package:smooth_app/themes/color_schemes.dart'; -import 'package:smooth_app/widgets/smooth_app_bar.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; import 'package:smooth_app/widgets/will_pop_scope.dart'; @@ -189,9 +188,11 @@ class _EditNewPackagingsState extends State child: UnfocusWhenTapOutside( child: SmoothScaffold( fixKeyboard: true, - appBar: SmoothAppBar( - title: Text(appLocalizations.edit_packagings_title), - subTitle: buildProductTitle(upToDateProduct, appLocalizations)), + appBar: buildEditProductAppBar( + context: context, + title: appLocalizations.edit_packagings_title, + product: upToDateProduct, + ), body: ListView( padding: const EdgeInsets.only(top: LARGE_SPACE), children: children, diff --git a/packages/smooth_app/lib/pages/product/edit_ocr_page.dart b/packages/smooth_app/lib/pages/product/edit_ocr_page.dart index 428ad4e2f2a..f5488774742 100644 --- a/packages/smooth_app/lib/pages/product/edit_ocr_page.dart +++ b/packages/smooth_app/lib/pages/product/edit_ocr_page.dart @@ -1,5 +1,3 @@ -import 'dart:ui' show ImageFilter; - import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; @@ -21,7 +19,6 @@ import 'package:smooth_app/pages/product/multilingual_helper.dart'; import 'package:smooth_app/pages/product/ocr_helper.dart'; import 'package:smooth_app/pages/product/product_image_local_button.dart'; import 'package:smooth_app/pages/product/product_image_server_button.dart'; -import 'package:smooth_app/widgets/smooth_app_bar.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; /// Editing with OCR a product field and the corresponding image. @@ -131,37 +128,13 @@ class _EditOcrPageState extends State with UpToDateMixin { _multilingualHelper.getCurrentLanguage(), ); - final TextStyle appbarTextStyle = TextStyle(shadows: [ - Shadow( - color: Theme.of(context).colorScheme.brightness == Brightness.light - ? Colors.white - : Colors.black, - offset: const Offset(0.5, 0.5), - blurRadius: 5.0, - ) - ]); - // TODO(monsieurtanuki): add WillPopScope / MayExitPage system return SmoothScaffold( extendBodyBehindAppBar: true, - appBar: SmoothAppBar( - centerTitle: false, - title: Text( - _helper.getTitle(appLocalizations), - style: appbarTextStyle, - ), - subTitle: DefaultTextStyle( - style: appbarTextStyle, - child: buildProductTitle(upToDateProduct, appLocalizations)), - backgroundColor: Colors.transparent, - flexibleSpace: ClipRect( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), - child: Container( - color: Colors.transparent, - ), - ), - ), + appBar: buildEditProductAppBar( + context: context, + title: _helper.getTitle(appLocalizations), + product: upToDateProduct, ), body: Stack( children: [ diff --git a/packages/smooth_app/lib/pages/product/edit_product_page.dart b/packages/smooth_app/lib/pages/product/edit_product_page.dart index bdadb818d30..5c489080618 100644 --- a/packages/smooth_app/lib/pages/product/edit_product_page.dart +++ b/packages/smooth_app/lib/pages/product/edit_product_page.dart @@ -154,7 +154,6 @@ class _EditProductPageState extends State with UpToDateMixin { ProductImageGalleryView( product: upToDateProduct, ), - fullscreenDialog: true, ), ); }, @@ -250,7 +249,6 @@ class _EditProductPageState extends State with UpToDateMixin { context, MaterialPageRoute( builder: (_) => AddOtherDetailsPage(upToDateProduct), - fullscreenDialog: true, ), ); }, @@ -308,7 +306,6 @@ class _EditProductPageState extends State with UpToDateMixin { helpers: helpers, product: upToDateProduct, ), - fullscreenDialog: true, ), ); }, diff --git a/packages/smooth_app/lib/pages/product/nutrition_page_loaded.dart b/packages/smooth_app/lib/pages/product/nutrition_page_loaded.dart index 10c02e7528f..52b59c45d46 100644 --- a/packages/smooth_app/lib/pages/product/nutrition_page_loaded.dart +++ b/packages/smooth_app/lib/pages/product/nutrition_page_loaded.dart @@ -22,7 +22,6 @@ import 'package:smooth_app/pages/product/nutrition_container.dart'; import 'package:smooth_app/pages/product/ordered_nutrients_cache.dart'; import 'package:smooth_app/pages/product/simple_input_number_field.dart'; import 'package:smooth_app/pages/text_field_helper.dart'; -import 'package:smooth_app/widgets/smooth_app_bar.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; import 'package:smooth_app/widgets/will_pop_scope.dart'; @@ -75,7 +74,6 @@ class NutritionPageLoaded extends StatefulWidget { cache.orderedNutrients, isLoggedInMandatory: isLoggedInMandatory, ), - fullscreenDialog: true, ), ); } @@ -197,12 +195,10 @@ class _NutritionPageLoadedState extends State onWillPop: () async => (await _mayExitPage(saving: false), null), child: SmoothScaffold( fixKeyboard: true, - appBar: SmoothAppBar( - title: AutoSizeText( - appLocalizations.nutrition_page_title, - maxLines: 1, - ), - subTitle: buildProductTitle(upToDateProduct, appLocalizations), + appBar: buildEditProductAppBar( + context: context, + title: appLocalizations.nutrition_page_title, + product: upToDateProduct, ), body: Padding( padding: const EdgeInsets.symmetric( diff --git a/packages/smooth_app/lib/pages/product/product_field_editor.dart b/packages/smooth_app/lib/pages/product/product_field_editor.dart index 8cbafdf1291..6c4f415b926 100644 --- a/packages/smooth_app/lib/pages/product/product_field_editor.dart +++ b/packages/smooth_app/lib/pages/product/product_field_editor.dart @@ -72,7 +72,6 @@ class ProductFieldSimpleEditor extends ProductFieldEditor { helper: helper, product: product, ), - fullscreenDialog: true, ), ); } @@ -163,7 +162,6 @@ class ProductFieldPackagingEditor extends ProductFieldEditor { product: product, isLoggedInMandatory: isLoggedInMandatory, ), - fullscreenDialog: true, ), ); } @@ -229,7 +227,6 @@ abstract class ProductFieldOcrEditor extends ProductFieldEditor { helper: helper, isLoggedInMandatory: isLoggedInMandatory, ), - fullscreenDialog: true, ), ); } diff --git a/packages/smooth_app/lib/pages/product/product_image_gallery_view.dart b/packages/smooth_app/lib/pages/product/product_image_gallery_view.dart index bc25612cdaf..e4b07e36f12 100644 --- a/packages/smooth_app/lib/pages/product/product_image_gallery_view.dart +++ b/packages/smooth_app/lib/pages/product/product_image_gallery_view.dart @@ -18,7 +18,6 @@ import 'package:smooth_app/pages/product/common/product_refresher.dart'; import 'package:smooth_app/pages/product/product_image_swipeable_view.dart'; import 'package:smooth_app/query/product_query.dart'; import 'package:smooth_app/widgets/slivers.dart'; -import 'package:smooth_app/widgets/smooth_app_bar.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; /// Display of the main 4 pictures of a product, with edit options. @@ -52,10 +51,10 @@ class _ProductImageGalleryViewState extends State context.watch(); refreshUpToDate(); return SmoothScaffold( - appBar: SmoothAppBar( - centerTitle: false, - title: Text(appLocalizations.edit_product_form_item_photos_title), - subTitle: buildProductTitle(upToDateProduct, appLocalizations), + appBar: buildEditProductAppBar( + context: context, + title: appLocalizations.edit_product_form_item_photos_title, + product: upToDateProduct, ), floatingActionButton: FloatingActionButton.extended( onPressed: () async { diff --git a/packages/smooth_app/lib/pages/product/simple_input_page.dart b/packages/smooth_app/lib/pages/product/simple_input_page.dart index 3508620cb56..90dc3d0d45e 100644 --- a/packages/smooth_app/lib/pages/product/simple_input_page.dart +++ b/packages/smooth_app/lib/pages/product/simple_input_page.dart @@ -13,7 +13,6 @@ import 'package:smooth_app/pages/product/common/product_buttons.dart'; import 'package:smooth_app/pages/product/may_exit_page_helper.dart'; import 'package:smooth_app/pages/product/simple_input_page_helpers.dart'; import 'package:smooth_app/pages/product/simple_input_widget.dart'; -import 'package:smooth_app/widgets/smooth_app_bar.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; import 'package:smooth_app/widgets/will_pop_scope.dart'; @@ -55,8 +54,10 @@ class _SimpleInputPageState extends State { Widget build(BuildContext context) { final AppLocalizations appLocalizations = AppLocalizations.of(context); final List simpleInputs = []; + final List titles = []; for (int i = 0; i < widget.helpers.length; i++) { + titles.add(widget.helpers[i].getTitle(appLocalizations)); simpleInputs.add( Padding( padding: i == 0 @@ -80,6 +81,7 @@ class _SimpleInputPageState extends State { helper: widget.helpers[i], product: widget.product, controller: _controllers[i], + displayTitle: widget.helpers.length > 1, ), ), ), @@ -92,13 +94,10 @@ class _SimpleInputPageState extends State { child: UnfocusWhenTapOutside( child: SmoothScaffold( fixKeyboard: true, - appBar: SmoothAppBar( - centerTitle: false, - title: buildProductTitle(widget.product, appLocalizations), - subTitle: widget.product.barcode != null - ? ExcludeSemantics( - excluding: true, child: Text(widget.product.barcode!)) - : null, + appBar: buildEditProductAppBar( + context: context, + title: titles.join(', '), + product: widget.product, ), body: Padding( padding: const EdgeInsets.all(SMALL_SPACE), diff --git a/packages/smooth_app/lib/pages/product/simple_input_widget.dart b/packages/smooth_app/lib/pages/product/simple_input_widget.dart index e2c529ef139..9c351f6ae83 100644 --- a/packages/smooth_app/lib/pages/product/simple_input_widget.dart +++ b/packages/smooth_app/lib/pages/product/simple_input_widget.dart @@ -14,11 +14,13 @@ class SimpleInputWidget extends StatefulWidget { required this.helper, required this.product, required this.controller, + required this.displayTitle, }); final AbstractSimpleInputPageHelper helper; final Product product; final TextEditingController controller; + final bool displayTitle; @override State createState() => _SimpleInputWidgetState(); @@ -64,15 +66,16 @@ class _SimpleInputWidgetState extends State { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - ListTile( - leading: widget.helper.getIcon(), - minLeadingWidth: 0.0, - horizontalTitleGap: 12.0, - title: Text( - widget.helper.getTitle(appLocalizations), - style: themeData.textTheme.displaySmall, + if (widget.displayTitle) + ListTile( + leading: widget.helper.getIcon(), + minLeadingWidth: 0.0, + horizontalTitleGap: 12.0, + title: Text( + widget.helper.getTitle(appLocalizations), + style: themeData.textTheme.displaySmall, + ), ), - ), if (explanations != null) ExplanationWidget(explanations), LayoutBuilder( builder: (_, BoxConstraints constraints) { diff --git a/packages/smooth_app/lib/pages/product/summary_card.dart b/packages/smooth_app/lib/pages/product/summary_card.dart index 698bc2f76f8..2a430b6e974 100644 --- a/packages/smooth_app/lib/pages/product/summary_card.dart +++ b/packages/smooth_app/lib/pages/product/summary_card.dart @@ -17,14 +17,12 @@ import 'package:smooth_app/helpers/product_cards_helper.dart'; import 'package:smooth_app/helpers/ui_helpers.dart'; import 'package:smooth_app/knowledge_panel/knowledge_panels/knowledge_panel_page.dart'; import 'package:smooth_app/knowledge_panel/knowledge_panels_builder.dart'; -import 'package:smooth_app/pages/product/add_simple_input_button.dart'; import 'package:smooth_app/pages/product/common/product_query_page_helper.dart'; import 'package:smooth_app/pages/product/hideable_container.dart'; import 'package:smooth_app/pages/product/product_compatibility_header.dart'; import 'package:smooth_app/pages/product/product_field_editor.dart'; import 'package:smooth_app/pages/product/product_incomplete_card.dart'; import 'package:smooth_app/pages/product/product_questions_widget.dart'; -import 'package:smooth_app/pages/product/simple_input_page_helpers.dart'; import 'package:smooth_app/pages/product/summary_attribute_group.dart'; import 'package:smooth_app/query/category_product_query.dart'; import 'package:smooth_app/query/product_query.dart'; @@ -294,17 +292,6 @@ class _SummaryCardState extends State with UpToDateMixin { final List summaryCardButtons = []; if (widget.isFullVersion) { - // Complete category - if (statesTags - .contains(ProductState.CATEGORIES_COMPLETED.toBeCompletedTag)) { - summaryCardButtons.add( - AddSimpleInputButton( - product: upToDateProduct, - helper: SimpleInputPageCategoryHelper(), - ), - ); - } - // Compare to category if (categoryTag != null && categoryLabel != null) { summaryCardButtons.add(