From dcd7dab1eba6cc57daf2ac81b3dea413267cc345 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Mon, 21 Oct 2024 10:29:22 +0200 Subject: [PATCH] feat: 5586 - added explicit product type to all relevant background tasks New file: * `work_type.dart`: Type of long download work for some background tasks. Impacted files: * `background_task.dart`: removed the default `uriProductHelper` getter * `background_task_barcode.dart`: added a `uriProductHelper` getter that depends on the `productType` * `background_task_download_products.dart`: refactored the access to product type * `background_task_full_refresh.dart`: split by product type * `background_task_language_refresh.dart`: split by product type * `background_task_offline.dart`: refactored the access to product type * `background_task_progressing.dart`: added the `productType` parameter; moved code to new `WorkType` class * `background_task_top_barcodes.dart`: refactored the access to product type * `dao_product.dart`: new methods `getProductTypes` and `splitAllProducts`; refactored with product type * `lazy_counter.dart`: explicitly counting the "food" products * `newsfeed_provider.dart`: explicitly getting the news from "food" * `offline_data_page.dart`: now displaying "download top N products" buttons for each product type; stats for each product type * `offline_tasks_page.dart`: enhanced "work text" algo, now depending on product type * `operation_type.dart`: enhanced "key" algo, now depending on product type * `ordered_nutrients_cache.dart`: explicitly using the "food" nutrients * `product_list_page.dart`: now reloading products from their server * `product_list_popup_items.dart`: now linking to the first server with products * `product_query.dart`: made product type a mandatory parameter * `product_refresher.dart`: added mandatory parameter product type * `random_questions_query.dart`: explicitly ask for "food" robotoff products * `temp_product_list_share_helper.dart`: added mandatory parameter product type * `user_preferences_dev_debug_info.dart`: added explicit use of "food" data --- .../lib/background/background_task.dart | 5 +- .../background/background_task_barcode.dart | 2 +- .../background_task_download_products.dart | 9 +- .../background_task_full_refresh.dart | 77 ++++++++------- .../background_task_language_refresh.dart | 32 ++++++- .../background/background_task_offline.dart | 10 +- .../background_task_progressing.dart | 25 +++-- .../background_task_top_barcodes.dart | 8 ++ .../lib/background/operation_type.dart | 5 +- .../smooth_app/lib/background/work_type.dart | 62 ++++++++++++ .../news_feed/newsfeed_provider.dart | 6 +- .../smooth_app/lib/database/dao_product.dart | 94 ++++++++++++++++--- .../temp_product_list_share_helper.dart | 7 +- .../lib/pages/offline_data_page.dart | 47 ++++++---- .../lib/pages/offline_tasks_page.dart | 19 ++-- .../lib/pages/preferences/lazy_counter.dart | 4 +- .../user_preferences_dev_debug_info.dart | 6 +- .../product/common/product_list_page.dart | 36 ++++--- .../common/product_list_popup_items.dart | 53 ++++++++--- .../product/common/product_refresher.dart | 6 +- .../product/ordered_nutrients_cache.dart | 8 +- .../smooth_app/lib/query/product_query.dart | 2 +- .../lib/query/random_questions_query.dart | 1 + 23 files changed, 389 insertions(+), 135 deletions(-) create mode 100644 packages/smooth_app/lib/background/work_type.dart diff --git a/packages/smooth_app/lib/background/background_task.dart b/packages/smooth_app/lib/background/background_task.dart index 5344986e3b6..09a166808a8 100644 --- a/packages/smooth_app/lib/background/background_task.dart +++ b/packages/smooth_app/lib/background/background_task.dart @@ -18,6 +18,7 @@ abstract class BackgroundTask { required this.stamp, final OpenFoodFactsLanguage? language, }) // TODO(monsieurtanuki): don't store the password in a clear format... +// TODO(monsieurtanuki): store the uriProductHelper as well : user = jsonEncode(ProductQuery.getWriteUser().toJson()), country = ProductQuery.getCountry().offTag, languageCode = (language ?? ProductQuery.getLanguage()).offTag; @@ -181,10 +182,6 @@ abstract class BackgroundTask { /// subtasks that call the next one at the end. bool get hasImmediateNextTask => false; -// TODO(monsieurtanuki): store the uriProductHelper as well - @protected - UriProductHelper get uriProductHelper => ProductQuery.getUriProductHelper(); - /// Returns true if tasks with the same stamp would overwrite each-other. bool isDeduplicable() => true; } diff --git a/packages/smooth_app/lib/background/background_task_barcode.dart b/packages/smooth_app/lib/background/background_task_barcode.dart index dcc36e0d1d4..203e5f4393c 100644 --- a/packages/smooth_app/lib/background/background_task_barcode.dart +++ b/packages/smooth_app/lib/background/background_task_barcode.dart @@ -56,7 +56,7 @@ abstract class BackgroundTaskBarcode extends BackgroundTask { localDatabase: localDatabase, ); - @override + @protected UriProductHelper get uriProductHelper => ProductQuery.getUriProductHelper( productType: productType, ); 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 99244ed3cd8..f3ad234c055 100644 --- a/packages/smooth_app/lib/background/background_task_download_products.dart +++ b/packages/smooth_app/lib/background/background_task_download_products.dart @@ -18,6 +18,7 @@ class BackgroundTaskDownloadProducts extends BackgroundTaskProgressing { required super.work, required super.pageSize, required super.totalSize, + required super.productType, required this.downloadFlag, }); @@ -49,12 +50,14 @@ class BackgroundTaskDownloadProducts extends BackgroundTaskProgressing { required final int totalSize, required final int soFarSize, required final int downloadFlag, + required final ProductType productType, }) async { final String uniqueId = await _operationType.getNewKey( localDatabase, soFarSize: soFarSize, totalSize: totalSize, work: work, + productType: productType, ); final BackgroundTask task = _getNewTask( uniqueId, @@ -62,6 +65,7 @@ class BackgroundTaskDownloadProducts extends BackgroundTaskProgressing { pageSize, totalSize, downloadFlag, + productType, ); await task.addToManager(localDatabase); } @@ -77,6 +81,7 @@ class BackgroundTaskDownloadProducts extends BackgroundTaskProgressing { final int pageSize, final int totalSize, final int downloadFlag, + final ProductType productType, ) => BackgroundTaskDownloadProducts._( processName: _operationType.processName, @@ -86,6 +91,7 @@ class BackgroundTaskDownloadProducts extends BackgroundTaskProgressing { pageSize: pageSize, totalSize: totalSize, downloadFlag: downloadFlag, + productType: productType, ); @override @@ -133,8 +139,6 @@ class BackgroundTaskDownloadProducts extends BackgroundTaskProgressing { throw Exception('Something bad happened downloading products'); } final DaoProduct daoProduct = DaoProduct(localDatabase); - final ProductType? productType = - ProductQuery.extractProductType(uriProductHelper); for (final Product product in downloadedProducts) { if (await _shouldBeUpdated(daoProduct, product.barcode!)) { await daoProduct.put( @@ -159,6 +163,7 @@ class BackgroundTaskDownloadProducts extends BackgroundTaskProgressing { totalSize: totalSize, soFarSize: totalSize - remaining, downloadFlag: downloadFlag, + productType: productType, ); } } diff --git a/packages/smooth_app/lib/background/background_task_full_refresh.dart b/packages/smooth_app/lib/background/background_task_full_refresh.dart index b11ddd1c030..a03f27dd362 100644 --- a/packages/smooth_app/lib/background/background_task_full_refresh.dart +++ b/packages/smooth_app/lib/background/background_task_full_refresh.dart @@ -5,8 +5,8 @@ import 'package:provider/provider.dart'; import 'package:smooth_app/background/background_task.dart'; import 'package:smooth_app/background/background_task_download_products.dart'; import 'package:smooth_app/background/background_task_paged.dart'; -import 'package:smooth_app/background/background_task_progressing.dart'; import 'package:smooth_app/background/operation_type.dart'; +import 'package:smooth_app/background/work_type.dart'; import 'package:smooth_app/database/dao_product.dart'; import 'package:smooth_app/database/dao_work_barcode.dart'; import 'package:smooth_app/database/local_database.dart'; @@ -66,34 +66,44 @@ class BackgroundTaskFullRefresh extends BackgroundTaskPaged { final DaoProduct daoProduct = DaoProduct(localDatabase); final DaoWorkBarcode daoWorkBarcode = DaoWorkBarcode(localDatabase); - await daoWorkBarcode - .deleteWork(BackgroundTaskProgressing.workFreshWithoutKP); - await daoWorkBarcode.deleteWork(BackgroundTaskProgressing.workFreshWithKP); + for (final ProductType productType in ProductType.values) { + await daoWorkBarcode.deleteWork( + WorkType.freshKP.getWorkTag(productType), + ); + await daoWorkBarcode.deleteWork( + WorkType.freshNoKP.getWorkTag(productType), + ); + } - // We separate the products into two lists, products with or without - // knowledge panels - final List barcodes = await daoProduct.getAllKeys(); - final List productsWithoutKP = []; - final List productsWithKP = []; - for (final String barcode in barcodes) { - if (await _shouldBeDownloadedWithoutKP(daoProduct, barcode)) { - productsWithoutKP.add(barcode); - } else { - productsWithKP.add(barcode); + // We separate the products into lists, products with or without + // knowledge panels, and split by product types. + final Map> split = await daoProduct.splitAllProducts( + (Product product) { + final bool noKP = product.knowledgePanels == null; + final WorkType workType = noKP ? WorkType.freshNoKP : WorkType.freshKP; + final ProductType productType = product.productType ?? ProductType.food; + return workType.getWorkTag(productType); + }, + ); + for (int i = 0; i <= 1; i++) { + final bool noKP = i == 0; + final WorkType workType = noKP ? WorkType.freshNoKP : WorkType.freshKP; + for (final ProductType productType in ProductType.values) { + final String tag = workType.getWorkTag(productType); + final List? barcodes = split[tag]; + if (barcodes == null) { + continue; + } + await _startDownloadTask( + barcodes: barcodes, + work: tag, + localDatabase: localDatabase, + downloadFlag: + noKP ? BackgroundTaskDownloadProducts.flagMaskExcludeKP : 0, + productType: productType, + ); } } - await _startDownloadTask( - barcodes: productsWithoutKP, - work: BackgroundTaskProgressing.workFreshWithoutKP, - localDatabase: localDatabase, - downloadFlag: BackgroundTaskDownloadProducts.flagMaskExcludeKP, - ); - await _startDownloadTask( - barcodes: productsWithKP, - work: BackgroundTaskProgressing.workFreshWithKP, - localDatabase: localDatabase, - downloadFlag: 0, - ); } @override @@ -102,24 +112,12 @@ class BackgroundTaskFullRefresh extends BackgroundTaskPaged { @override bool hasImmediateNextTask = false; - /// Returns true if we should download data without KP. - /// - /// That happens in one case: - /// * we already have a corresponding local product that does not have - /// populated knowledge panel fields. - static Future _shouldBeDownloadedWithoutKP( - final DaoProduct daoProduct, - final String barcode, - ) async { - final Product? product = await daoProduct.get(barcode); - return product != null && product.knowledgePanels == null; - } - Future _startDownloadTask({ required final List barcodes, required final String work, required final LocalDatabase localDatabase, required final int downloadFlag, + required final ProductType productType, }) async { if (barcodes.isEmpty) { return; @@ -134,6 +132,7 @@ class BackgroundTaskFullRefresh extends BackgroundTaskPaged { totalSize: barcodes.length, soFarSize: 0, downloadFlag: downloadFlag, + productType: productType, ); } } diff --git a/packages/smooth_app/lib/background/background_task_language_refresh.dart b/packages/smooth_app/lib/background/background_task_language_refresh.dart index cec03a1f8aa..cd0c88056c3 100644 --- a/packages/smooth_app/lib/background/background_task_language_refresh.dart +++ b/packages/smooth_app/lib/background/background_task_language_refresh.dart @@ -14,10 +14,15 @@ class BackgroundTaskLanguageRefresh extends BackgroundTask { required super.uniqueId, required super.stamp, required this.excludeBarcodes, + required this.productType, }); BackgroundTaskLanguageRefresh.fromJson(super.json) : excludeBarcodes = _getStringList(json, _jsonTagExcludeBarcodes), + productType = + ProductType.fromOffTag(json[_jsonTagProductType] as String?) ?? +// for legacy reason (not refreshed products = no product type) + ProductType.food, super.fromJson(); static List _getStringList( @@ -32,28 +37,48 @@ class BackgroundTaskLanguageRefresh extends BackgroundTask { } final List excludeBarcodes; + final ProductType productType; static const String _jsonTagExcludeBarcodes = 'excludeBarcodes'; + static const String _jsonTagProductType = 'productType'; @override Map toJson() { final Map result = super.toJson(); result[_jsonTagExcludeBarcodes] = excludeBarcodes; + result[_jsonTagProductType] = productType.offTag; return result; } static const OperationType _operationType = OperationType.languageRefresh; + UriProductHelper get _uriProductHelper => ProductQuery.getUriProductHelper( + productType: productType, + ); + static Future addTask( final LocalDatabase localDatabase, { final List excludeBarcodes = const [], + final ProductType? productType, }) async { + if (productType == null) { + for (final ProductType item in ProductType.values) { + await addTask( + localDatabase, + excludeBarcodes: excludeBarcodes, + productType: item, + ); + } + return; + } final String uniqueId = await _operationType.getNewKey( localDatabase, + productType: productType, ); final BackgroundTask task = _getNewTask( uniqueId, excludeBarcodes, + productType, ); await task.addToManager(localDatabase); } @@ -66,12 +91,14 @@ class BackgroundTaskLanguageRefresh extends BackgroundTask { static BackgroundTask _getNewTask( final String uniqueId, final List excludeBarcodes, + final ProductType productType, ) => BackgroundTaskLanguageRefresh._( processName: _operationType.processName, uniqueId: uniqueId, - stamp: ';languageRefresh', + stamp: ';languageRefresh;${productType.offTag}', excludeBarcodes: excludeBarcodes, + productType: productType, ); @override @@ -91,6 +118,7 @@ class BackgroundTaskLanguageRefresh extends BackgroundTask { language, limit: _pageSize, excludeBarcodes: excludeBarcodes, + productType: productType, ); if (barcodes.isEmpty) { return; @@ -108,7 +136,7 @@ class BackgroundTaskLanguageRefresh extends BackgroundTask { country: ProductQuery.getCountry(), version: ProductQuery.productQueryVersion, ), - uriHelper: uriProductHelper, + uriHelper: _uriProductHelper, ); if (searchResult.products == null || searchResult.count == null) { throw Exception('Cannot refresh language'); diff --git a/packages/smooth_app/lib/background/background_task_offline.dart b/packages/smooth_app/lib/background/background_task_offline.dart index a8e2b03929f..fba57b31924 100644 --- a/packages/smooth_app/lib/background/background_task_offline.dart +++ b/packages/smooth_app/lib/background/background_task_offline.dart @@ -1,10 +1,12 @@ 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/background/background_task.dart'; import 'package:smooth_app/background/background_task_progressing.dart'; import 'package:smooth_app/background/background_task_top_barcodes.dart'; import 'package:smooth_app/background/operation_type.dart'; +import 'package:smooth_app/background/work_type.dart'; import 'package:smooth_app/database/dao_work_barcode.dart'; import 'package:smooth_app/database/local_database.dart'; @@ -17,6 +19,7 @@ class BackgroundTaskOffline extends BackgroundTaskProgressing { required super.work, required super.pageSize, required super.totalSize, + required super.productType, }); BackgroundTaskOffline.fromJson(super.json) : super.fromJson(); @@ -27,6 +30,7 @@ class BackgroundTaskOffline extends BackgroundTaskProgressing { required final BuildContext context, required final int pageSize, required final int totalSize, + required final ProductType productType, }) async { final LocalDatabase localDatabase = context.read(); final String uniqueId = await _operationType.getNewKey( @@ -36,9 +40,10 @@ class BackgroundTaskOffline extends BackgroundTaskProgressing { ); final BackgroundTask task = _getNewTask( uniqueId, - BackgroundTaskProgressing.workOffline, + WorkType.offline.getWorkTag(productType), pageSize, totalSize, + productType, ); if (!context.mounted) { return; @@ -59,6 +64,7 @@ class BackgroundTaskOffline extends BackgroundTaskProgressing { final String work, final int pageSize, final int totalSize, + final ProductType productType, ) => BackgroundTaskOffline._( processName: _operationType.processName, @@ -67,6 +73,7 @@ class BackgroundTaskOffline extends BackgroundTaskProgressing { work: work, pageSize: pageSize, totalSize: totalSize, + productType: productType, ); @override @@ -85,6 +92,7 @@ class BackgroundTaskOffline extends BackgroundTaskProgressing { pageSize: pageSize, totalSize: totalSize, soFarSize: 0, + productType: productType, ); } } diff --git a/packages/smooth_app/lib/background/background_task_progressing.dart b/packages/smooth_app/lib/background/background_task_progressing.dart index dca7c12b3ac..c7e92dabf47 100644 --- a/packages/smooth_app/lib/background/background_task_progressing.dart +++ b/packages/smooth_app/lib/background/background_task_progressing.dart @@ -1,4 +1,7 @@ +import 'package:flutter/foundation.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:smooth_app/background/background_task_paged.dart'; +import 'package:smooth_app/query/product_query.dart'; /// Abstract background task with work in progress actions. abstract class BackgroundTaskProgressing extends BackgroundTaskPaged { @@ -9,35 +12,39 @@ abstract class BackgroundTaskProgressing extends BackgroundTaskPaged { required super.pageSize, required this.work, required this.totalSize, + required this.productType, }); BackgroundTaskProgressing.fromJson(super.json) : work = json[_jsonTagWork] as String, totalSize = json[_jsonTagTotalSize] as int, + productType = + ProductType.fromOffTag(json[_jsonTagProductType] as String?) ?? +// for legacy reason (not refreshed products = no product type) + ProductType.food, super.fromJson(); final String work; final int totalSize; + final ProductType productType; static const String _jsonTagWork = 'work'; static const String _jsonTagTotalSize = 'totalSize'; + static const String _jsonTagProductType = 'productType'; @override Map toJson() { final Map result = super.toJson(); result[_jsonTagWork] = work; result[_jsonTagTotalSize] = totalSize; + result[_jsonTagProductType] = productType.offTag; return result; } - static const String noBarcode = 'NO_BARCODE'; - - /// Work about downloading top products. - static const String workOffline = 'O'; + @protected + UriProductHelper get uriProductHelper => ProductQuery.getUriProductHelper( + productType: productType, + ); - /// Work about downloading fresh products with Knowledge Panels. - static const String workFreshWithKP = 'K'; - - /// Work about downloading fresh products without Knowledge Panels. - static const String workFreshWithoutKP = 'w'; + static const String noBarcode = 'NO_BARCODE'; } diff --git a/packages/smooth_app/lib/background/background_task_top_barcodes.dart b/packages/smooth_app/lib/background/background_task_top_barcodes.dart index bf00f099e56..846422bf81d 100644 --- a/packages/smooth_app/lib/background/background_task_top_barcodes.dart +++ b/packages/smooth_app/lib/background/background_task_top_barcodes.dart @@ -18,6 +18,7 @@ class BackgroundTaskTopBarcodes extends BackgroundTaskProgressing { required super.work, required super.pageSize, required super.totalSize, + required super.productType, required this.pageNumber, }); @@ -44,12 +45,14 @@ class BackgroundTaskTopBarcodes extends BackgroundTaskProgressing { required final int pageSize, required final int totalSize, required final int soFarSize, + required final ProductType productType, final int pageNumber = 1, }) async { final String uniqueId = await _operationType.getNewKey( localDatabase, totalSize: totalSize, soFarSize: soFarSize, + productType: productType, ); final BackgroundTask task = _getNewTask( uniqueId, @@ -57,6 +60,7 @@ class BackgroundTaskTopBarcodes extends BackgroundTaskProgressing { pageSize, totalSize, pageNumber, + productType, ); await task.addToManager(localDatabase); } @@ -72,6 +76,7 @@ class BackgroundTaskTopBarcodes extends BackgroundTaskProgressing { final int pageSize, final int totalSize, final int pageNumber, + final ProductType productType, ) => BackgroundTaskTopBarcodes._( processName: _operationType.processName, @@ -81,6 +86,7 @@ class BackgroundTaskTopBarcodes extends BackgroundTaskProgressing { pageSize: pageSize, totalSize: totalSize, pageNumber: pageNumber, + productType: productType, ); @override @@ -131,6 +137,7 @@ class BackgroundTaskTopBarcodes extends BackgroundTaskProgressing { totalSize: newTotalSize, soFarSize: soFarAfter, pageNumber: pageNumber + 1, + productType: productType, ); } else { // we have all the barcodes; now we need to download the products. @@ -141,6 +148,7 @@ class BackgroundTaskTopBarcodes extends BackgroundTaskProgressing { totalSize: soFarAfter, soFarSize: 0, downloadFlag: BackgroundTaskDownloadProducts.flagMaskExcludeKP, + productType: productType, ); } } diff --git a/packages/smooth_app/lib/background/operation_type.dart b/packages/smooth_app/lib/background/operation_type.dart index f1d83e874ce..c86ca37b6d2 100644 --- a/packages/smooth_app/lib/background/operation_type.dart +++ b/packages/smooth_app/lib/background/operation_type.dart @@ -1,4 +1,5 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:smooth_app/background/background_task.dart'; import 'package:smooth_app/background/background_task_add_other_price.dart'; import 'package:smooth_app/background/background_task_add_price.dart'; @@ -58,6 +59,7 @@ enum OperationType { final int? totalSize, final int? soFarSize, final String? work, + final ProductType? productType, }) async { final int sequentialId = await getNextSequenceNumber(DaoInt(localDatabase), _uniqueSequenceKey); @@ -66,7 +68,8 @@ enum OperationType { '$_transientHeaderSeparator$barcode' '$_transientHeaderSeparator${totalSize == null ? '' : totalSize.toString()}' '$_transientHeaderSeparator${soFarSize == null ? '' : soFarSize.toString()}' - '$_transientHeaderSeparator${work ?? ''}'; + '$_transientHeaderSeparator${work ?? ''}' + '$_transientHeaderSeparator${productType == null ? '' : productType.offTag}'; } BackgroundTask fromJson(Map map) => switch (this) { diff --git a/packages/smooth_app/lib/background/work_type.dart b/packages/smooth_app/lib/background/work_type.dart new file mode 100644 index 00000000000..257a6e8af1e --- /dev/null +++ b/packages/smooth_app/lib/background/work_type.dart @@ -0,0 +1,62 @@ +import 'package:openfoodfacts/openfoodfacts.dart'; + +/// Type of long download work for some background tasks. +enum WorkType { + /// Top products. + offline( + tag: 'O', + englishLabel: 'Top products', + ), + + /// Fresh products with Knowledge Panels. + freshKP( + tag: 'K', + englishLabel: 'Refresh products with KP', + ), + + /// Fresh products without Knowledge Panels. + freshNoKP( + tag: 'w', + englishLabel: 'Refresh products without KP', + ); + + const WorkType({ + required this.tag, + required this.englishLabel, + }); + + final String tag; + final String englishLabel; + + String getWorkTag(final ProductType productType) => + '$tag:${productType.offTag}'; + + static (WorkType, ProductType)? extract(final String string) { + if (string.isEmpty) { + return null; + } + final List strings = string.split(':'); + if (strings.length > 2) { + return null; + } + final ProductType productType; + if (strings.length == 1) { + productType = ProductType.food; + } else { + productType = ProductType.fromOffTag(strings[1])!; + } + final WorkType workType = fromTag(strings[0])!; + return (workType, productType); + } + + static WorkType? fromTag( + final String tag, + ) { + for (final WorkType workType in values) { + if (workType.tag == tag) { + return workType; + } + } + return null; + } +} diff --git a/packages/smooth_app/lib/data_models/news_feed/newsfeed_provider.dart b/packages/smooth_app/lib/data_models/news_feed/newsfeed_provider.dart index 2fe18d8d600..231f4375191 100644 --- a/packages/smooth_app/lib/data_models/news_feed/newsfeed_provider.dart +++ b/packages/smooth_app/lib/data_models/news_feed/newsfeed_provider.dart @@ -107,7 +107,9 @@ class AppNewsProvider extends ChangeNotifier { Future _fetchJSON() async { try { final UriProductHelper uriProductHelper = - ProductQuery.getUriProductHelper(); + ProductQuery.getUriProductHelper( + productType: ProductType.food, + ); final Map headers = {}; final Uri uri; @@ -169,7 +171,7 @@ class AppNewsProvider extends ChangeNotifier { String? _domain; String? _uriOverride; - /// [ProductQuery.uriProductHelper] is not synced yet, + /// [ProductQuery._uriProductHelper] is not synced yet, /// so we have to check it manually Future _onPreferencesChanged() async { final String jsonURI = _preferences.getDevModeString( diff --git a/packages/smooth_app/lib/database/dao_product.dart b/packages/smooth_app/lib/database/dao_product.dart index de35e277776..1b677ae7ae9 100644 --- a/packages/smooth_app/lib/database/dao_product.dart +++ b/packages/smooth_app/lib/database/dao_product.dart @@ -92,6 +92,65 @@ class DaoProduct extends AbstractSqlDao implements BulkDeletable { return result; } + /// Returns the local products split by product type. + Future>> getProductTypes( + final List barcodes, + ) async { + final Map> result = >{}; + if (barcodes.isEmpty) { + return result; + } + for (int start = 0; + start < barcodes.length; + start += BulkManager.SQLITE_MAX_VARIABLE_NUMBER) { + final int size = min( + barcodes.length - start, + BulkManager.SQLITE_MAX_VARIABLE_NUMBER, + ); + final List> queryResults = + await localDatabase.database.query( + _TABLE_PRODUCT, + columns: _columns, + where: '$_TABLE_PRODUCT_COLUMN_BARCODE in(? ${',?' * (size - 1)})', + whereArgs: barcodes.sublist(start, start + size), + ); + for (final Map row in queryResults) { + final Product product = _getProductFromQueryResult(row); + final ProductType productType = product.productType ?? ProductType.food; + List? barcodes = result[productType]; + if (barcodes == null) { + barcodes = []; + result[productType] = barcodes; + } + barcodes.add(product.barcode!); + } + } + return result; + } + + /// Returns all the local products split by a function. + Future>> splitAllProducts( + final String Function(Product) splitFunction, + ) async { + final Map> result = >{}; + final List> queryResults = + await localDatabase.database.query( + _TABLE_PRODUCT, + columns: _columns, + ); + for (final Map row in queryResults) { + final Product product = _getProductFromQueryResult(row); + final String splitValue = splitFunction(product); + List? barcodes = result[splitValue]; + if (barcodes == null) { + barcodes = []; + result[splitValue] = barcodes; + } + barcodes.add(product.barcode!); + } + return result; + } + Future put( final Product product, final OpenFoodFactsLanguage language, { @@ -232,13 +291,20 @@ class DaoProduct extends AbstractSqlDao implements BulkDeletable { } /// Get the total number of products in the database - Future getTotalNoOfProducts() async { - return Sqflite.firstIntValue( - await localDatabase.database.rawQuery( - 'select count(*) from $_TABLE_PRODUCT', - ), - ) ?? - 0; + Future> getTotalNoOfProducts() async { + final Map result = {}; + final List> queryResults = + await localDatabase.database.query( + _TABLE_PRODUCT, + columns: _columns, + ); + for (final Map row in queryResults) { + final Product product = _getProductFromQueryResult(row); + final ProductType productType = product.productType ?? ProductType.food; + final int? count = result[productType]; + result[productType] = 1 + (count ?? 0); + } + return result; } /// Get the estimated total size of the database in MegaBytes @@ -277,10 +343,11 @@ class DaoProduct extends AbstractSqlDao implements BulkDeletable { final OpenFoodFactsLanguage language, { required final int limit, required final List excludeBarcodes, + required final ProductType productType, }) async { /// Unfortunately, some SQFlite implementations don't support "nulls last" String getRawQuery(final bool withNullsLast) => - 'select p.$_TABLE_PRODUCT_COLUMN_BARCODE ' + 'select p.$_TABLE_PRODUCT_COLUMN_GZIPPED_JSON ' 'from' ' $_TABLE_PRODUCT p' ' left outer join ${DaoProductLastAccess.TABLE} a' @@ -288,8 +355,7 @@ class DaoProduct extends AbstractSqlDao implements BulkDeletable { 'where' ' p.$_TABLE_PRODUCT_COLUMN_LANGUAGE is null' ' or p.$_TABLE_PRODUCT_COLUMN_LANGUAGE != ? ' - 'order by a.${DaoProductLastAccess.COLUMN_LAST_ACCESS} desc ${withNullsLast ? 'nulls last' : ''} ' - 'limit ?'; + 'order by a.${DaoProductLastAccess.COLUMN_LAST_ACCESS} desc ${withNullsLast ? 'nulls last' : ''} '; List> queryResults = >[]; try { @@ -297,7 +363,6 @@ class DaoProduct extends AbstractSqlDao implements BulkDeletable { getRawQuery(true), [ language.offTag, - limit + excludeBarcodes.length, ], ); } catch (e) { @@ -310,7 +375,6 @@ class DaoProduct extends AbstractSqlDao implements BulkDeletable { getRawQuery(false), [ language.offTag, - limit + excludeBarcodes.length, ], ); } @@ -318,10 +382,14 @@ class DaoProduct extends AbstractSqlDao implements BulkDeletable { final List result = []; for (final Map row in queryResults) { - final String barcode = row[_TABLE_PRODUCT_COLUMN_BARCODE] as String; + final Product product = _getProductFromQueryResult(row); + final String barcode = product.barcode!; if (excludeBarcodes.contains(barcode)) { continue; } + if ((product.productType ?? ProductType.food) != productType) { + continue; + } result.add(barcode); if (result.length == limit) { break; diff --git a/packages/smooth_app/lib/helpers/temp_product_list_share_helper.dart b/packages/smooth_app/lib/helpers/temp_product_list_share_helper.dart index d2adeb878e9..09f99228783 100644 --- a/packages/smooth_app/lib/helpers/temp_product_list_share_helper.dart +++ b/packages/smooth_app/lib/helpers/temp_product_list_share_helper.dart @@ -2,11 +2,14 @@ import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:smooth_app/query/product_query.dart'; // TODO(m123): Move this to off-dart -Uri shareProductList(List barcodes) { +Uri shareProductList( + final List barcodes, + final ProductType productType, +) { final String barcodesString = barcodes.join(','); return UriHelper.replaceSubdomain( - ProductQuery.getUriProductHelper().getUri( + ProductQuery.getUriProductHelper(productType: productType).getUri( path: 'products/$barcodesString', addUserAgentParameters: false, ), diff --git a/packages/smooth_app/lib/pages/offline_data_page.dart b/packages/smooth_app/lib/pages/offline_data_page.dart index ee3a26d37e8..382e502fbd5 100644 --- a/packages/smooth_app/lib/pages/offline_data_page.dart +++ b/packages/smooth_app/lib/pages/offline_data_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:provider/provider.dart'; import 'package:smooth_app/background/background_task_full_refresh.dart'; import 'package:smooth_app/background/background_task_offline.dart'; @@ -10,6 +11,7 @@ import 'package:smooth_app/database/local_database.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/duration_constants.dart'; import 'package:smooth_app/helpers/app_helper.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'; @@ -60,16 +62,19 @@ class _OfflineDataPageState extends State { _StatsWidget( daoProduct: daoProduct, ), - _OfflinePageListTile( - title: appLocalizations.download_data, - subtitle: appLocalizations.download_top_n_products(_topNSize), - onTap: () async => BackgroundTaskOffline.addTask( - context: context, - pageSize: _pageSize, - totalSize: _topNSize, + for (final ProductType productType in ProductType.values) + _OfflinePageListTile( + title: + '${appLocalizations.download_data} (${productType.getLabel(appLocalizations)})', + subtitle: appLocalizations.download_top_n_products(_topNSize), + onTap: () async => BackgroundTaskOffline.addTask( + context: context, + pageSize: _pageSize, + totalSize: _topNSize, + productType: productType, + ), + trailing: const Icon(Icons.download), ), - trailing: const Icon(Icons.download), - ), _OfflinePageListTile( title: appLocalizations.update_offline_data, subtitle: appLocalizations.update_local_database_sub, @@ -129,16 +134,26 @@ class _StatsWidget extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: SMALL_SPACE), child: ListTile( title: Text(applocalizations.offline_product_data_title), - subtitle: FutureBuilder( + subtitle: FutureBuilder>( future: daoProduct.getTotalNoOfProducts(), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return Text( - applocalizations.available_for_download(snapshot.data!), - ); - } else { + builder: ( + BuildContext context, + AsyncSnapshot> snapshot, + ) { + if (!snapshot.hasData) { return Text(applocalizations.loading); } + int count = 0; + final List list = []; + for (final MapEntry item + in snapshot.data!.entries) { + count += item.value; + list.add( + '${item.value} (${item.key.getLabel(applocalizations)})'); + } + return Text( + '${applocalizations.available_for_download(count)} ${list.join(', ')}', + ); }, ), trailing: FutureBuilder( diff --git a/packages/smooth_app/lib/pages/offline_tasks_page.dart b/packages/smooth_app/lib/pages/offline_tasks_page.dart index 327598cd0b1..21fec7668f9 100644 --- a/packages/smooth_app/lib/pages/offline_tasks_page.dart +++ b/packages/smooth_app/lib/pages/offline_tasks_page.dart @@ -1,9 +1,11 @@ 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/background/background_task_manager.dart'; import 'package:smooth_app/background/background_task_progressing.dart'; import 'package:smooth_app/background/operation_type.dart'; +import 'package:smooth_app/background/work_type.dart'; import 'package:smooth_app/database/dao_instant_string.dart'; import 'package:smooth_app/database/local_database.dart'; import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart'; @@ -126,16 +128,13 @@ class _OfflineTaskState extends State { String? _getWorkText(final String taskId) { final String? work = OperationType.getWork(taskId); - switch (work) { - case null: - case '': - return null; - case BackgroundTaskProgressing.workOffline: - return 'Top products'; - case BackgroundTaskProgressing.workFreshWithoutKP: - return 'Refresh products without KP'; - case BackgroundTaskProgressing.workFreshWithKP: - return 'Refresh products with KP'; + if (work == null || work.isEmpty) { + return null; + } + final (WorkType workType, ProductType productType)? item = + WorkType.extract(work); + if (item != null) { + return '${item.$1.englishLabel} (${item.$2.offTag})'; } return 'Unknown work ($work)!'; } diff --git a/packages/smooth_app/lib/pages/preferences/lazy_counter.dart b/packages/smooth_app/lib/pages/preferences/lazy_counter.dart index a14432500ea..3d530e9cfa6 100644 --- a/packages/smooth_app/lib/pages/preferences/lazy_counter.dart +++ b/packages/smooth_app/lib/pages/preferences/lazy_counter.dart @@ -84,7 +84,9 @@ class LazyCounterUserSearch extends LazyCounter { final SearchResult result = await OpenFoodAPIClient.searchProducts( user, configuration, - uriHelper: ProductQuery.getUriProductHelper(), + uriHelper: ProductQuery.getUriProductHelper( + productType: ProductType.food, + ), ); return result.count; } catch (e) { diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_dev_debug_info.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_dev_debug_info.dart index 38507150fa9..eb536edf0c3 100644 --- a/packages/smooth_app/lib/pages/preferences/user_preferences_dev_debug_info.dart +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_dev_debug_info.dart @@ -29,10 +29,12 @@ class _UserPreferencesDebugInfoState extends State { 'IsLoggedIn': ProductQuery.isLoggedIn().toString(), 'UUID': OpenFoodAPIConfiguration.uuid.toString(), 'Matomo Visitor ID': AnalyticsHelper.matomoVisitorId, - 'QueryType': ProductQuery.getUriProductHelper().isTestMode + 'QueryType': ProductQuery.getUriProductHelper(productType: ProductType.food) + .isTestMode ? 'QueryType.TEST' : 'QueryType.PROD', - 'Domain': ProductQuery.getUriProductHelper().domain, + 'Domain': + ProductQuery.getUriProductHelper(productType: ProductType.food).domain, 'UserAgent-name': '${OpenFoodAPIConfiguration.userAgent?.name}', 'UserAgent-system': '${OpenFoodAPIConfiguration.userAgent?.system}', }; 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 d9f89d8bd28..edf7293dec0 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 @@ -447,26 +447,34 @@ class _ProductListPageState extends State final List barcodes, final LocalDatabase localDatabase, ) async { + bool fresh = true; try { final OpenFoodFactsLanguage language = ProductQuery.getLanguage(); - final SearchResult searchResult = await OpenFoodAPIClient.searchProducts( - ProductQuery.getReadUser(), - ProductRefresher().getBarcodeListQueryConfiguration( - barcodes, - language, - ), - uriHelper: ProductQuery.getUriProductHelper(), - ); - final List? freshProducts = searchResult.products; - if (freshProducts == null) { - return false; + final Map> productTypes = + await DaoProduct(localDatabase).getProductTypes(barcodes); + for (final MapEntry> entry + in productTypes.entries) { + final SearchResult searchResult = + await OpenFoodAPIClient.searchProducts( + ProductQuery.getReadUser(), + ProductRefresher().getBarcodeListQueryConfiguration( + entry.value, + language, + ), + uriHelper: ProductQuery.getUriProductHelper(productType: entry.key), + ); + final List? freshProducts = searchResult.products; + if (freshProducts == null) { + fresh = false; + } else { + await DaoProduct(localDatabase).putAll(freshProducts, language); + localDatabase.upToDate.setLatestDownloadedProducts(freshProducts); + } } - await DaoProduct(localDatabase).putAll(freshProducts, language); - localDatabase.upToDate.setLatestDownloadedProducts(freshProducts); final RobotoffInsightHelper robotoffInsightHelper = RobotoffInsightHelper(localDatabase); await robotoffInsightHelper.clearInsightAnnotationsSaved(); - return true; + return fresh; } catch (e) { // } diff --git a/packages/smooth_app/lib/pages/product/common/product_list_popup_items.dart b/packages/smooth_app/lib/pages/product/common/product_list_popup_items.dart index fbbd20e1796..d52e1f82729 100644 --- a/packages/smooth_app/lib/pages/product/common/product_list_popup_items.dart +++ b/packages/smooth_app/lib/pages/product/common/product_list_popup_items.dart @@ -1,7 +1,11 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:share_plus/share_plus.dart'; import 'package:smooth_app/data_models/product_list.dart'; +import 'package:smooth_app/database/dao_product.dart'; import 'package:smooth_app/database/dao_product_list.dart'; import 'package:smooth_app/database/local_database.dart'; import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart'; @@ -53,6 +57,22 @@ abstract class ProductListPopupItem { label: getTitle(appLocalizations), type: isDestructive() ? SmoothPopupMenuItemType.destructive : null, ); + + /// Returns the first possible URL/server that contains at least one product. + @protected + Future _getFirstUrl({ + required final ProductList productList, + required final LocalDatabase localDatabase, + }) async { + final List products = productList.getList(); + final Map> productTypes = + await DaoProduct(localDatabase).getProductTypes(products); + for (final MapEntry> entry + in productTypes.entries) { + return shareProductList(entry.value, entry.key); + } + return null; + } } /// Popup menu item for the product list page: clear list. @@ -147,15 +167,21 @@ class ProductListPopupShare extends ProductListPopupItem { required final BuildContext context, }) async { final AppLocalizations appLocalizations = AppLocalizations.of(context); - final List products = productList.getList(); - final String url = shareProductList(products).toString(); - final RenderBox? box = context.findRenderObject() as RenderBox?; - AnalyticsHelper.trackEvent(AnalyticsEvent.shareList); - Share.share( - appLocalizations.share_product_list_text(url), - sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, - ); + final String? url = (await _getFirstUrl( + productList: productList, + localDatabase: localDatabase, + )) + ?.toString(); + if (url != null) { + AnalyticsHelper.trackEvent(AnalyticsEvent.shareList); + unawaited( + Share.share( + appLocalizations.share_product_list_text(url), + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + ), + ); + } return null; } } @@ -179,9 +205,14 @@ class ProductListPopupOpenInWeb extends ProductListPopupItem { required final LocalDatabase localDatabase, required final BuildContext context, }) async { - final List products = productList.getList(); - AnalyticsHelper.trackEvent(AnalyticsEvent.openListWeb); - await launchUrl(shareProductList(products)); + final Uri? firstUrl = await _getFirstUrl( + productList: productList, + localDatabase: localDatabase, + ); + if (firstUrl != null) { + AnalyticsHelper.trackEvent(AnalyticsEvent.openListWeb); + unawaited(launchUrl(firstUrl)); + } return null; } } 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 be1553bbf4e..12d9b338d07 100644 --- a/packages/smooth_app/lib/pages/product/common/product_refresher.dart +++ b/packages/smooth_app/lib/pages/product/common/product_refresher.dart @@ -107,8 +107,9 @@ class ProductRefresher { Future silentFetchAndRefreshList({ required final List barcodes, required final LocalDatabase localDatabase, + required final ProductType productType, }) async => - _fetchAndRefreshList(localDatabase, barcodes); + _fetchAndRefreshList(localDatabase, barcodes, productType); /// Fetches the product from the server and refreshes the local database. /// @@ -246,13 +247,14 @@ class ProductRefresher { Future _fetchAndRefreshList( final LocalDatabase localDatabase, final List barcodes, + final ProductType productType, ) async { try { final OpenFoodFactsLanguage language = ProductQuery.getLanguage(); final SearchResult searchResult = await OpenFoodAPIClient.searchProducts( ProductQuery.getReadUser(), getBarcodeListQueryConfiguration(barcodes, language), - uriHelper: ProductQuery.getUriProductHelper(), + uriHelper: ProductQuery.getUriProductHelper(productType: productType), ); if (searchResult.products == null) { return null; diff --git a/packages/smooth_app/lib/pages/product/ordered_nutrients_cache.dart b/packages/smooth_app/lib/pages/product/ordered_nutrients_cache.dart index 1c08aa7d3a3..ec0dd4e688e 100644 --- a/packages/smooth_app/lib/pages/product/ordered_nutrients_cache.dart +++ b/packages/smooth_app/lib/pages/product/ordered_nutrients_cache.dart @@ -54,12 +54,16 @@ class OrderedNutrientsCache { return null; } + UriProductHelper get _uriProductHelper => ProductQuery.getUriProductHelper( + productType: ProductType.food, + ); + /// Downloads the ordered nutrients and caches them in the database. Future _download() async { final String string = await OpenFoodAPIClient.getOrderedNutrientsJsonString( country: ProductQuery.getCountry(), language: ProductQuery.getLanguage(), - uriHelper: ProductQuery.getUriProductHelper(), + uriHelper: _uriProductHelper, ); final OrderedNutrients result = OrderedNutrients.fromJson( jsonDecode(string) as Map, @@ -75,6 +79,6 @@ class OrderedNutrientsCache { return 'nutrients.pl' '/${country.offTag}' '/${language.code}' - '/${ProductQuery.getUriProductHelper().domain}'; + '/${_uriProductHelper.domain}'; } } diff --git a/packages/smooth_app/lib/query/product_query.dart b/packages/smooth_app/lib/query/product_query.dart index 5a892f89e1f..315942d1ecf 100644 --- a/packages/smooth_app/lib/query/product_query.dart +++ b/packages/smooth_app/lib/query/product_query.dart @@ -216,7 +216,7 @@ abstract class ProductQuery { // TODO(monsieurtanuki): make the parameter "required" static UriProductHelper getUriProductHelper({ - final ProductType? productType, + required final ProductType? productType, }) { final UriProductHelper currentUriProductHelper = _uriProductHelper; if (productType == null) { diff --git a/packages/smooth_app/lib/query/random_questions_query.dart b/packages/smooth_app/lib/query/random_questions_query.dart index 15bdd7c0bad..b368b4a79ba 100644 --- a/packages/smooth_app/lib/query/random_questions_query.dart +++ b/packages/smooth_app/lib/query/random_questions_query.dart @@ -31,6 +31,7 @@ class RandomQuestionsQuery extends QuestionsQuery { await ProductRefresher().silentFetchAndRefreshList( barcodes: barcodes, localDatabase: localDatabase, + productType: ProductType.food, ); return result.questions ?? []; }