From 077bf5ef8c1b51335118a9b88bc11e89dd89f9cd Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Fri, 27 Sep 2024 22:40:14 +0200 Subject: [PATCH] feat: 5586 - OxF filter for term searches (#5637) Impacted files: * `category_product_query.dart`: refactored with new `productType` parameter * `keywords_product_query.dart`: refactored with new `productType` parameter * `paged_product_query.dart`: refactored with new `productType` parameter * `paged_search_product_query.dart`: refactored with new `productType` parameter * `paged_to_be_completed_product_query.dart`: refactored with new `productType` parameter * `paged_user_product_query.dart`: refactored with new `productType` parameter * `product_list.dart`: refactored with new `productType` parameter * `product_query.dart`: added a "get label" method for `ProductType` * `search_field.dart`: displayed the new optional "additional filter" (e.g. OxF) * `search_helper.dart`: added a new optional "additional filter" * `search_product_helper.dart`: new "product type filter" widget * `summary_card.dart`: minor "productType" fix * `user_preferences_account.dart`: added "productType.food" to user page counts * `user_preferences_contribute.dart`: added "productType.food" to "to be completed" products --- .../lib/data_models/product_list.dart | 21 +++++++- .../preferences/user_preferences_account.dart | 9 +++- .../user_preferences_contribute.dart | 6 ++- .../pages/product/common/search_helper.dart | 2 + .../lib/pages/product/summary_card.dart | 5 +- .../lib/pages/search/search_field.dart | 32 ++++++++----- .../pages/search/search_product_helper.dart | 48 ++++++++++++++++++- .../lib/query/category_product_query.dart | 17 +++++-- .../lib/query/keywords_product_query.dart | 17 +++++-- .../lib/query/paged_product_query.dart | 8 +++- .../lib/query/paged_search_product_query.dart | 5 +- .../paged_to_be_completed_product_query.dart | 15 ++++-- .../lib/query/paged_user_product_query.dart | 6 +++ .../smooth_app/lib/query/product_query.dart | 4 ++ 14 files changed, 164 insertions(+), 31 deletions(-) diff --git a/packages/smooth_app/lib/data_models/product_list.dart b/packages/smooth_app/lib/data_models/product_list.dart index 6e155d6db98..b0df3db6912 100644 --- a/packages/smooth_app/lib/data_models/product_list.dart +++ b/packages/smooth_app/lib/data_models/product_list.dart @@ -47,6 +47,7 @@ class ProductList { this.pageNumber = 0, this.language, this.country, + this.productType, }); ProductList.keywordSearch( @@ -55,6 +56,7 @@ class ProductList { required int pageNumber, required OpenFoodFactsLanguage language, required OpenFoodFactsCountry? country, + required ProductType productType, }) : this._( listType: ProductListType.HTTP_SEARCH_KEYWORDS, parameters: keywords, @@ -62,6 +64,7 @@ class ProductList { pageNumber: pageNumber, language: language, country: country, + productType: productType, ); ProductList.categorySearch( @@ -70,6 +73,7 @@ class ProductList { required int pageNumber, required OpenFoodFactsLanguage language, required OpenFoodFactsCountry? country, + required ProductType productType, }) : this._( listType: ProductListType.HTTP_SEARCH_CATEGORY, parameters: category, @@ -77,6 +81,7 @@ class ProductList { pageNumber: pageNumber, language: language, country: country, + productType: productType, ); ProductList.contributor( @@ -84,12 +89,14 @@ class ProductList { required int pageSize, required int pageNumber, required OpenFoodFactsLanguage language, + required ProductType productType, }) : this._( listType: ProductListType.HTTP_USER_CONTRIBUTOR, parameters: userId, pageSize: pageSize, pageNumber: pageNumber, language: language, + productType: productType, ); ProductList.informer( @@ -97,12 +104,14 @@ class ProductList { required int pageSize, required int pageNumber, required OpenFoodFactsLanguage language, + required ProductType productType, }) : this._( listType: ProductListType.HTTP_USER_INFORMER, parameters: userId, pageSize: pageSize, pageNumber: pageNumber, language: language, + productType: productType, ); ProductList.photographer( @@ -110,12 +119,14 @@ class ProductList { required int pageSize, required int pageNumber, required OpenFoodFactsLanguage language, + required ProductType productType, }) : this._( listType: ProductListType.HTTP_USER_PHOTOGRAPHER, parameters: userId, pageSize: pageSize, pageNumber: pageNumber, language: language, + productType: productType, ); ProductList.toBeCompleted( @@ -123,12 +134,14 @@ class ProductList { required int pageSize, required int pageNumber, required OpenFoodFactsLanguage language, + required ProductType productType, }) : this._( listType: ProductListType.HTTP_USER_TO_BE_COMPLETED, parameters: userId, pageSize: pageSize, pageNumber: pageNumber, language: language, + productType: productType, ); ProductList.allToBeCompleted({ @@ -136,12 +149,14 @@ class ProductList { required int pageNumber, required OpenFoodFactsLanguage language, required OpenFoodFactsCountry? country, + required ProductType productType, }) : this._( listType: ProductListType.HTTP_ALL_TO_BE_COMPLETED, pageSize: pageSize, pageNumber: pageNumber, language: language, country: country, + productType: productType, ); ProductList.history() : this._(listType: ProductListType.HISTORY); @@ -171,6 +186,9 @@ class ProductList { /// Country at query time. final OpenFoodFactsCountry? country; + /// ProductType at query time. + final ProductType? productType; + /// "Total size" returned by the query. int totalSize = 0; @@ -251,7 +269,8 @@ class ProductList { ',$pageSize' ',$pageNumber' ',${language?.code ?? ''}' - ',${country?.offTag ?? ''}'; + ',${country?.offTag ?? ''}' + '${productType == null || productType == ProductType.food ? '' : ',${productType!.offTag}'}'; } } diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart index f89897d60f9..2418130f07d 100644 --- a/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart @@ -171,6 +171,8 @@ class UserPreferencesAccount extends AbstractUserPreferences { productQuery: PagedUserProductQuery( userId: userId, type: UserSearchType.CONTRIBUTOR, + // TODO(monsieurtanuki): only food? + productType: ProductType.food, ), title: appLocalizations.user_search_contributor_title, iconData: Icons.add_circle_outline, @@ -182,6 +184,7 @@ class UserPreferencesAccount extends AbstractUserPreferences { productQuery: PagedUserProductQuery( userId: userId, type: UserSearchType.INFORMER, + productType: ProductType.food, ), title: appLocalizations.user_search_informer_title, iconData: Icons.edit, @@ -193,6 +196,7 @@ class UserPreferencesAccount extends AbstractUserPreferences { productQuery: PagedUserProductQuery( userId: userId, type: UserSearchType.PHOTOGRAPHER, + productType: ProductType.food, ), title: appLocalizations.user_search_photographer_title, iconData: Icons.add_a_photo, @@ -204,6 +208,7 @@ class UserPreferencesAccount extends AbstractUserPreferences { productQuery: PagedUserProductQuery( userId: userId, type: UserSearchType.TO_BE_COMPLETED, + productType: ProductType.food, ), title: appLocalizations.user_search_to_be_completed_title, iconData: Icons.more_horiz, @@ -296,7 +301,9 @@ class UserPreferencesAccount extends AbstractUserPreferences { 'app/products', ), _buildProductQueryTile( - productQuery: PagedToBeCompletedProductQuery(), + productQuery: PagedToBeCompletedProductQuery( + productType: ProductType.food, + ), title: appLocalizations.all_search_to_be_completed_title, iconData: Icons.more_outlined, context: context, diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_contribute.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_contribute.dart index 5807d949c80..d0e438e2c1c 100644 --- a/packages/smooth_app/lib/pages/preferences/user_preferences_contribute.dart +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_contribute.dart @@ -4,6 +4,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:http/http.dart' as http; +import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:provider/provider.dart'; import 'package:share_plus/share_plus.dart'; import 'package:smooth_app/data_models/github_contributors_model.dart'; @@ -170,7 +171,10 @@ class UserPreferencesContribute extends AbstractUserPreferences { ProductQueryPageHelper.openBestChoice( name: appLocalizations.all_search_to_be_completed_title, localDatabase: localDatabase, - productQuery: PagedToBeCompletedProductQuery(), + productQuery: PagedToBeCompletedProductQuery( + // TODO(monsieurtanuki): only food? + productType: ProductType.food, + ), // the other "context"s being popped context: this.context, editableAppBarTitle: false, diff --git a/packages/smooth_app/lib/pages/product/common/search_helper.dart b/packages/smooth_app/lib/pages/product/common/search_helper.dart index fb171d7779a..872ff254cfc 100644 --- a/packages/smooth_app/lib/pages/product/common/search_helper.dart +++ b/packages/smooth_app/lib/pages/product/common/search_helper.dart @@ -26,6 +26,8 @@ abstract class SearchHelper extends ValueNotifier { /// Hint text for the search field. String getHintText(final AppLocalizations appLocalizations); + Widget? getAdditionalFilter() => null; + /// Returns all the previous queries, in reverse order. List getAllQueries(final LocalDatabase localDatabase) => DaoStringList(localDatabase).getAll(historyKey).reversed.toList(); diff --git a/packages/smooth_app/lib/pages/product/summary_card.dart b/packages/smooth_app/lib/pages/product/summary_card.dart index df54b727673..e11021e2a68 100644 --- a/packages/smooth_app/lib/pages/product/summary_card.dart +++ b/packages/smooth_app/lib/pages/product/summary_card.dart @@ -301,7 +301,10 @@ class _SummaryCardState extends State with UpToDateMixin { onPressed: () async => ProductQueryPageHelper.openBestChoice( name: categoryLabel!, localDatabase: context.read(), - productQuery: CategoryProductQuery(categoryTag!), + productQuery: CategoryProductQuery( + categoryTag!, + productType: upToDateProduct.productType ?? ProductType.food, + ), context: context, searchResult: false, ), diff --git a/packages/smooth_app/lib/pages/search/search_field.dart b/packages/smooth_app/lib/pages/search/search_field.dart index baefe1ab293..dee1658d520 100644 --- a/packages/smooth_app/lib/pages/search/search_field.dart +++ b/packages/smooth_app/lib/pages/search/search_field.dart @@ -73,6 +73,7 @@ class _SearchFieldState extends State { final TextStyle textStyle = SearchFieldUIHelper.textStyle(context); + final Widget? additionalFilter = widget.searchHelper.getAdditionalFilter(); return ChangeNotifierProvider.value( value: _controller!, child: SmoothHero( @@ -90,19 +91,24 @@ class _SearchFieldState extends State { : null, child: Material( // ↑ Needed by the Hero Widget - child: TextField( - controller: _controller, - focusNode: _focusNode, - onSubmitted: (String query) => _performSearch(context, query), - textInputAction: TextInputAction.search, - enableSuggestions: widget.enableSuggestions, - autocorrect: widget.autocorrect, - style: textStyle, - decoration: _getInputDecoration( - context, - localizations, - ), - cursorColor: textStyle.color, + child: Column( + children: [ + TextField( + controller: _controller, + focusNode: _focusNode, + onSubmitted: (String query) => _performSearch(context, query), + textInputAction: TextInputAction.search, + enableSuggestions: widget.enableSuggestions, + autocorrect: widget.autocorrect, + style: textStyle, + decoration: _getInputDecoration( + context, + localizations, + ), + cursorColor: textStyle.color, + ), + if (additionalFilter != null) additionalFilter, + ], ), ), ), diff --git a/packages/smooth_app/lib/pages/search/search_product_helper.dart b/packages/smooth_app/lib/pages/search/search_product_helper.dart index d2f6b3a71dd..91e0a87ced3 100644 --- a/packages/smooth_app/lib/pages/search/search_product_helper.dart +++ b/packages/smooth_app/lib/pages/search/search_product_helper.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:provider/provider.dart'; import 'package:smooth_app/data_models/fetched_product.dart'; import 'package:smooth_app/database/dao_string_list.dart'; @@ -12,11 +13,15 @@ import 'package:smooth_app/pages/product/common/product_dialog_helper.dart'; import 'package:smooth_app/pages/product/common/product_query_page_helper.dart'; import 'package:smooth_app/pages/product/common/search_helper.dart'; import 'package:smooth_app/query/keywords_product_query.dart'; +import 'package:smooth_app/query/product_query.dart'; /// Search helper dedicated to product search. class SearchProductHelper extends SearchHelper { SearchProductHelper(); + // TODO(monsieurtanuki): maybe reinit it with latest value + ProductType _productType = ProductType.food; + @override String get historyKey => DaoStringList.keySearchProductHistory; @@ -24,6 +29,9 @@ class SearchProductHelper extends SearchHelper { String getHintText(final AppLocalizations appLocalizations) => appLocalizations.search; + @override + Widget getAdditionalFilter() => _ProductTypeFilter(this); + @override void search( BuildContext context, @@ -71,6 +79,7 @@ class SearchProductHelper extends SearchHelper { final FetchedProduct fetchedProduct = await productDialogHelper.openBestChoice(); if (fetchedProduct.status == FetchedProductStatus.ok) { + // TODO(monsieurtanuki): add OxF to Matomo data? AnalyticsHelper.trackSearch( search: value, searchCategory: 'barcode', @@ -95,7 +104,6 @@ class SearchProductHelper extends SearchHelper { } } -// used to be in now defunct `ChoosePage` Future _onSubmittedText( final String value, final BuildContext context, @@ -107,7 +115,10 @@ class SearchProductHelper extends SearchHelper { widget: await ProductQueryPageHelper.getBestChoiceWidget( name: value, localDatabase: localDatabase, - productQuery: KeywordsProductQuery(value), + productQuery: KeywordsProductQuery( + value, + productType: _productType, + ), context: context, editableAppBarTitle: false, ), @@ -115,3 +126,36 @@ class SearchProductHelper extends SearchHelper { ); } } + +class _ProductTypeFilter extends StatefulWidget { + const _ProductTypeFilter(this.searchProductHelper); + + final SearchProductHelper searchProductHelper; + + @override + State<_ProductTypeFilter> createState() => _ProductTypeFilterState(); +} + +class _ProductTypeFilterState extends State<_ProductTypeFilter> { + @override + Widget build(BuildContext context) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + final List> segments = + >[]; + for (final ProductType productType in ProductType.values) { + segments.add( + ButtonSegment( + value: productType, + label: Text(productType.getLabel(appLocalizations)), + ), + ); + } + return SegmentedButton( + segments: segments, + selected: {widget.searchProductHelper._productType}, + onSelectionChanged: (Set newSelection) => setState( + () => widget.searchProductHelper._productType = newSelection.first, + ), + ); + } +} diff --git a/packages/smooth_app/lib/query/category_product_query.dart b/packages/smooth_app/lib/query/category_product_query.dart index 9b11249bfbe..9b996d42f84 100644 --- a/packages/smooth_app/lib/query/category_product_query.dart +++ b/packages/smooth_app/lib/query/category_product_query.dart @@ -5,7 +5,11 @@ import 'package:smooth_app/query/paged_search_product_query.dart'; /// Back-end query about a category. class CategoryProductQuery extends PagedSearchProductQuery { - CategoryProductQuery(this.categoryTag, {super.world}); + CategoryProductQuery( + this.categoryTag, { + required super.productType, + super.world, + }); // e.g. 'en:unsweetened-natural-soy-milks' final String categoryTag; @@ -24,6 +28,7 @@ class CategoryProductQuery extends PagedSearchProductQuery { pageNumber: pageNumber, language: language, country: country, + productType: productType, ); @override @@ -33,11 +38,17 @@ class CategoryProductQuery extends PagedSearchProductQuery { ', $pageNumber' ', $language' ', $country' + ', $productType' ')'; @override - PagedProductQuery? getWorldQuery() => - world ? null : CategoryProductQuery(categoryTag, world: true); + PagedProductQuery? getWorldQuery() => world + ? null + : CategoryProductQuery( + categoryTag, + world: true, + productType: productType, + ); @override bool hasDifferentCountryWorldData() => true; diff --git a/packages/smooth_app/lib/query/keywords_product_query.dart b/packages/smooth_app/lib/query/keywords_product_query.dart index 1fae1e7e867..d2cde92f448 100644 --- a/packages/smooth_app/lib/query/keywords_product_query.dart +++ b/packages/smooth_app/lib/query/keywords_product_query.dart @@ -5,7 +5,11 @@ import 'package:smooth_app/query/paged_search_product_query.dart'; /// Back-end query around user-entered keywords. class KeywordsProductQuery extends PagedSearchProductQuery { - KeywordsProductQuery(this.keywords, {super.world}); + KeywordsProductQuery( + this.keywords, { + required super.productType, + super.world, + }); final String keywords; @@ -19,6 +23,7 @@ class KeywordsProductQuery extends PagedSearchProductQuery { pageNumber: pageNumber, language: language, country: country, + productType: productType, ); @override @@ -28,11 +33,17 @@ class KeywordsProductQuery extends PagedSearchProductQuery { ', $pageNumber' ', $language' ', $country' + ', $productType' ')'; @override - PagedProductQuery? getWorldQuery() => - world ? null : KeywordsProductQuery(keywords, world: true); + PagedProductQuery? getWorldQuery() => world + ? null + : KeywordsProductQuery( + keywords, + productType: productType, + world: true, + ); @override bool hasDifferentCountryWorldData() => true; diff --git a/packages/smooth_app/lib/query/paged_product_query.dart b/packages/smooth_app/lib/query/paged_product_query.dart index 4ef51cd4abc..978a588f7ee 100644 --- a/packages/smooth_app/lib/query/paged_product_query.dart +++ b/packages/smooth_app/lib/query/paged_product_query.dart @@ -4,8 +4,12 @@ import 'package:smooth_app/query/product_query.dart'; /// Paged product query (with [pageSize] and [pageNumber]). abstract class PagedProductQuery { - PagedProductQuery({this.world = false}); + PagedProductQuery({ + required this.productType, + this.world = false, + }); + final ProductType productType; final int pageSize = _typicalPageSize; /// Likely to change: to next page, and back to top. @@ -38,7 +42,7 @@ abstract class PagedProductQuery { OpenFoodAPIClient.searchProducts( ProductQuery.getReadUser(), getQueryConfiguration(), - uriHelper: ProductQuery.getUriProductHelper(), + uriHelper: ProductQuery.getUriProductHelper(productType: productType), ); AbstractQueryConfiguration getQueryConfiguration(); diff --git a/packages/smooth_app/lib/query/paged_search_product_query.dart b/packages/smooth_app/lib/query/paged_search_product_query.dart index d983c32997f..b4973bd752a 100644 --- a/packages/smooth_app/lib/query/paged_search_product_query.dart +++ b/packages/smooth_app/lib/query/paged_search_product_query.dart @@ -4,7 +4,10 @@ import 'package:smooth_app/query/product_query.dart'; /// Back-end paged queries around search. abstract class PagedSearchProductQuery extends PagedProductQuery { - PagedSearchProductQuery({super.world}); + PagedSearchProductQuery({ + required super.productType, + super.world, + }); Parameter getParameter(); diff --git a/packages/smooth_app/lib/query/paged_to_be_completed_product_query.dart b/packages/smooth_app/lib/query/paged_to_be_completed_product_query.dart index 574b519abea..8115feeed31 100644 --- a/packages/smooth_app/lib/query/paged_to_be_completed_product_query.dart +++ b/packages/smooth_app/lib/query/paged_to_be_completed_product_query.dart @@ -5,7 +5,10 @@ import 'package:smooth_app/query/product_query.dart'; /// Back-end paged query for all "to-be-completed" products. class PagedToBeCompletedProductQuery extends PagedProductQuery { - PagedToBeCompletedProductQuery({super.world}); + PagedToBeCompletedProductQuery({ + required super.productType, + super.world, + }); @override AbstractQueryConfiguration getQueryConfiguration() => @@ -32,6 +35,7 @@ class PagedToBeCompletedProductQuery extends PagedProductQuery { pageNumber: pageNumber, language: language, country: country, + productType: productType, ); @override @@ -40,11 +44,16 @@ class PagedToBeCompletedProductQuery extends PagedProductQuery { ', $pageNumber' ', $language' ', $country' + ', $productType' ')'; @override - PagedProductQuery? getWorldQuery() => - world ? null : PagedToBeCompletedProductQuery(world: true); + PagedProductQuery? getWorldQuery() => world + ? null + : PagedToBeCompletedProductQuery( + productType: productType, + world: true, + ); @override bool hasDifferentCountryWorldData() => true; diff --git a/packages/smooth_app/lib/query/paged_user_product_query.dart b/packages/smooth_app/lib/query/paged_user_product_query.dart index fe9e22b0bf0..7d4e48b107b 100644 --- a/packages/smooth_app/lib/query/paged_user_product_query.dart +++ b/packages/smooth_app/lib/query/paged_user_product_query.dart @@ -55,6 +55,7 @@ class PagedUserProductQuery extends PagedProductQuery { PagedUserProductQuery({ required this.userId, required this.type, + required super.productType, }); final String userId; @@ -78,6 +79,7 @@ class PagedUserProductQuery extends PagedProductQuery { pageSize: pageSize, pageNumber: pageNumber, language: language, + productType: productType, ); case UserSearchType.INFORMER: return ProductList.informer( @@ -85,6 +87,7 @@ class PagedUserProductQuery extends PagedProductQuery { pageSize: pageSize, pageNumber: pageNumber, language: language, + productType: productType, ); case UserSearchType.PHOTOGRAPHER: return ProductList.photographer( @@ -92,6 +95,7 @@ class PagedUserProductQuery extends PagedProductQuery { pageSize: pageSize, pageNumber: pageNumber, language: language, + productType: productType, ); case UserSearchType.TO_BE_COMPLETED: return ProductList.toBeCompleted( @@ -99,6 +103,7 @@ class PagedUserProductQuery extends PagedProductQuery { pageSize: pageSize, pageNumber: pageNumber, language: language, + productType: productType, ); } } @@ -110,5 +115,6 @@ class PagedUserProductQuery extends PagedProductQuery { ', $pageSize' ', $pageNumber' ', $language' + ', $productType' ')'; } diff --git a/packages/smooth_app/lib/query/product_query.dart b/packages/smooth_app/lib/query/product_query.dart index a9046bd1011..af0902ec1e6 100644 --- a/packages/smooth_app/lib/query/product_query.dart +++ b/packages/smooth_app/lib/query/product_query.dart @@ -1,6 +1,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:smooth_app/data_models/preferences/user_preferences.dart'; @@ -299,4 +300,7 @@ extension ProductTypeExtension on ProductType { ProductType.petFood => 'openpetfoodfacts', ProductType.product => 'openproductsfacts', }; + + // TODO(monsieurtanuki): localize with very short names, or use icons instead + String getLabel(final AppLocalizations appLocalizations) => name; }