diff --git a/packages/smooth_app/lib/data_models/preferences/user_preferences.dart b/packages/smooth_app/lib/data_models/preferences/user_preferences.dart index f5c4d3aa077..168ccac1d8e 100644 --- a/packages/smooth_app/lib/data_models/preferences/user_preferences.dart +++ b/packages/smooth_app/lib/data_models/preferences/user_preferences.dart @@ -81,6 +81,7 @@ class UserPreferences extends ChangeNotifier { static const String _TAG_EXCLUDED_ATTRIBUTE_IDS = 'excluded_attributes'; static const String _TAG_USER_GROUP = '_user_group'; static const String _TAG_UNIQUE_RANDOM = '_unique_random'; + static const String _TAG_LAZY_COUNT_PREFIX = '_lazy_count_prefix'; /// Camera preferences @@ -173,6 +174,14 @@ class UserPreferences extends ChangeNotifier { notifyListeners(); } + String _getLazyCountTag(final String tag) => '$_TAG_LAZY_COUNT_PREFIX$tag'; + + Future setLazyCount(final int value, final String suffixTag) async => + _sharedPreferences.setInt(_getLazyCountTag(suffixTag), value); + + int? getLazyCount(final String suffixTag) => + _sharedPreferences.getInt(_getLazyCountTag(suffixTag)); + Future setUserTracking(final bool state) async { await _sharedPreferences.setBool(_TAG_USER_TRACKING, state); onAnalyticsChanged.value = state; diff --git a/packages/smooth_app/lib/pages/preferences/lazy_counter.dart b/packages/smooth_app/lib/pages/preferences/lazy_counter.dart new file mode 100644 index 00000000000..68ae09dd87d --- /dev/null +++ b/packages/smooth_app/lib/pages/preferences/lazy_counter.dart @@ -0,0 +1,93 @@ +import 'package:flutter/foundation.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; +import 'package:smooth_app/data_models/preferences/user_preferences.dart'; +import 'package:smooth_app/query/paged_user_product_query.dart'; +import 'package:smooth_app/query/product_query.dart'; +import 'package:smooth_app/services/smooth_services.dart'; + +/// Lazy Counter, with a cached value stored locally, and a call to the server. +abstract class LazyCounter { + const LazyCounter(); + + /// Returns the value cached locally; + int? getLocalCount(final UserPreferences userPreferences) => + userPreferences.getLazyCount(getSuffixTag()); + + /// Sets the value cached locally; + Future setLocalCount( + final int value, + final UserPreferences userPreferences, + ) => + userPreferences.setLazyCount(value, getSuffixTag()); + + /// Returns the suffix tag used to cache the value locally; + @protected + String getSuffixTag(); + + /// Gets the latest value from the server. + Future getServerCount(); +} + +/// Lazy Counter dedicated to Prices counts. +class LazyCounterPrices extends LazyCounter { + const LazyCounterPrices(this.owner); + + final String? owner; + + @override + String getSuffixTag() => 'P_$owner'; + + @override + Future getServerCount() async { + final MaybeError result = + await OpenPricesAPIClient.getPrices( + GetPricesParameters() + ..owner = owner + ..pageSize = 1, + uriHelper: ProductQuery.uriPricesHelper, + ); + if (result.isError) { + return null; + } + return result.value.total; + } +} + +/// Lazy Counter dedicated to OFF User Search counts. +class LazyCounterUserSearch extends LazyCounter { + const LazyCounterUserSearch(this.type); + + final UserSearchType type; + + @override + String getSuffixTag() => 'US_$type'; + + @override + Future getServerCount() async { + final User user = ProductQuery.getWriteUser(); + final ProductSearchQueryConfiguration configuration = type.getConfiguration( + user.userId, + 1, + 1, + ProductQuery.getLanguage(), + // one field is enough as we want only the count + // and we need at least one field (no field meaning all fields) + [ProductField.BARCODE], + ); + + try { + final SearchResult result = await OpenFoodAPIClient.searchProducts( + user, + configuration, + uriHelper: ProductQuery.uriProductHelper, + ); + return result.count; + } catch (e) { + Logs.e( + 'Could not count the number of products for $type, ${user.userId}', + ex: e, + ); + return null; + } + } +} diff --git a/packages/smooth_app/lib/pages/preferences/lazy_counter_widget.dart b/packages/smooth_app/lib/pages/preferences/lazy_counter_widget.dart new file mode 100644 index 00000000000..d3861fe8c39 --- /dev/null +++ b/packages/smooth_app/lib/pages/preferences/lazy_counter_widget.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:smooth_app/data_models/preferences/user_preferences.dart'; +import 'package:smooth_app/pages/preferences/lazy_counter.dart'; + +/// Widget displaying a Lazy Counter: cached value, refresh button, and loading. +class LazyCounterWidget extends StatefulWidget { + const LazyCounterWidget(this.lazyCounter); + + final LazyCounter lazyCounter; + + @override + State createState() => _LazyCounterWidgetState(); +} + +class _LazyCounterWidgetState extends State { + bool _loading = false; + int? _count; + + @override + void initState() { + super.initState(); + final UserPreferences userPreferences = context.read(); + _count = widget.lazyCounter.getLocalCount(userPreferences); + if (_count == null) { + _asyncLoad(); + } + } + + @override + Widget build(BuildContext context) => Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (_count != null) Text(_count.toString()), + if (_loading) + const Padding( + padding: EdgeInsets.symmetric(horizontal: 12), + child: SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator.adaptive(), + ), + ) + else + IconButton( + onPressed: () => _asyncLoad(), + icon: const Icon(Icons.refresh), + ), + ], + ); + + Future _asyncLoad() async { + if (_loading) { + return; + } + _loading = true; + final UserPreferences userPreferences = context.read(); + if (mounted) { + setState(() {}); + } + try { + final int? value = await widget.lazyCounter.getServerCount(); + if (value != null) { + await widget.lazyCounter.setLocalCount(value, userPreferences); + _count = value; + } + } catch (e) { + // + } finally { + _loading = false; + if (mounted) { + setState(() {}); + } + } + } +} 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 f6b7a9ac755..72e31af9874 100644 --- a/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart @@ -14,6 +14,8 @@ import 'package:smooth_app/helpers/launch_url_helper.dart'; import 'package:smooth_app/helpers/user_management_helper.dart'; import 'package:smooth_app/pages/preferences/abstract_user_preferences.dart'; import 'package:smooth_app/pages/preferences/account_deletion_webview.dart'; +import 'package:smooth_app/pages/preferences/lazy_counter.dart'; +import 'package:smooth_app/pages/preferences/lazy_counter_widget.dart'; import 'package:smooth_app/pages/preferences/user_preferences_item.dart'; import 'package:smooth_app/pages/preferences/user_preferences_list_tile.dart'; import 'package:smooth_app/pages/preferences/user_preferences_page.dart'; @@ -29,7 +31,6 @@ import 'package:smooth_app/query/paged_product_query.dart'; import 'package:smooth_app/query/paged_to_be_completed_product_query.dart'; import 'package:smooth_app/query/paged_user_product_query.dart'; import 'package:smooth_app/query/product_query.dart'; -import 'package:smooth_app/services/smooth_services.dart'; class UserPreferencesAccount extends AbstractUserPreferences { UserPreferencesAccount({ @@ -182,7 +183,7 @@ class UserPreferencesAccount extends AbstractUserPreferences { iconData: Icons.add_circle_outline, context: context, localDatabase: localDatabase, - myCount: _getMyCount(UserSearchType.CONTRIBUTOR), + lazyCounter: const LazyCounterUserSearch(UserSearchType.CONTRIBUTOR), ), _buildProductQueryTile( productQuery: PagedUserProductQuery( @@ -193,7 +194,7 @@ class UserPreferencesAccount extends AbstractUserPreferences { iconData: Icons.edit, context: context, localDatabase: localDatabase, - myCount: _getMyCount(UserSearchType.INFORMER), + lazyCounter: const LazyCounterUserSearch(UserSearchType.INFORMER), ), _buildProductQueryTile( productQuery: PagedUserProductQuery( @@ -204,7 +205,7 @@ class UserPreferencesAccount extends AbstractUserPreferences { iconData: Icons.add_a_photo, context: context, localDatabase: localDatabase, - myCount: _getMyCount(UserSearchType.PHOTOGRAPHER), + lazyCounter: const LazyCounterUserSearch(UserSearchType.PHOTOGRAPHER), ), _buildProductQueryTile( productQuery: PagedUserProductQuery( @@ -215,7 +216,8 @@ class UserPreferencesAccount extends AbstractUserPreferences { iconData: Icons.more_horiz, context: context, localDatabase: localDatabase, - myCount: _getMyCount(UserSearchType.TO_BE_COMPLETED), + lazyCounter: + const LazyCounterUserSearch(UserSearchType.TO_BE_COMPLETED), ), _getListTile( PriceUserButton.showUserTitle( @@ -227,7 +229,7 @@ class UserPreferencesAccount extends AbstractUserPreferences { context: context, ), CupertinoIcons.money_dollar_circle, - myCount: _getPricesCount(owner: ProductQuery.getWriteUser().userId), + lazyCounter: LazyCounterPrices(ProductQuery.getWriteUser().userId), ), _getListTile( appLocalizations.user_search_proofs_title, @@ -281,7 +283,7 @@ class UserPreferencesAccount extends AbstractUserPreferences { ), ), CupertinoIcons.money_dollar_circle, - myCount: _getPricesCount(), + lazyCounter: const LazyCounterPrices(null), ), _getListTile( appLocalizations.all_search_prices_top_user_title, @@ -386,57 +388,13 @@ class UserPreferencesAccount extends AbstractUserPreferences { }, ); - Future _getMyCount( - final UserSearchType type, - ) async { - final User user = ProductQuery.getWriteUser(); - final ProductSearchQueryConfiguration configuration = type.getConfiguration( - user.userId, - 1, - 1, - ProductQuery.getLanguage(), - // one field is enough as we want only the count - // and we need at least one field (no field meaning all fields) - [ProductField.BARCODE], - ); - - try { - final SearchResult result = await OpenFoodAPIClient.searchProducts( - user, - configuration, - uriHelper: ProductQuery.uriProductHelper, - ); - return result.count; - } catch (e) { - Logs.e( - 'Could not count the number of products for $type, ${user.userId}', - ex: e, - ); - return null; - } - } - - Future _getPricesCount({final String? owner}) async { - final MaybeError result = - await OpenPricesAPIClient.getPrices( - GetPricesParameters() - ..owner = owner - ..pageSize = 1, - uriHelper: ProductQuery.uriPricesHelper, - ); - if (result.isError) { - return null; - } - return result.value.total; - } - UserPreferencesItem _buildProductQueryTile({ required final PagedProductQuery productQuery, required final String title, required final IconData iconData, required final BuildContext context, required final LocalDatabase localDatabase, - final Future? myCount, + final LazyCounter? lazyCounter, }) => _getListTile( title, @@ -448,14 +406,14 @@ class UserPreferencesAccount extends AbstractUserPreferences { editableAppBarTitle: false, ), iconData, - myCount: myCount, + lazyCounter: lazyCounter, ); UserPreferencesItem _getListTile( final String title, final VoidCallback onTap, final IconData leading, { - final Future? myCount, + final LazyCounter? lazyCounter, }) => UserPreferencesItemSimple( labels: [title], @@ -472,23 +430,8 @@ class UserPreferencesAccount extends AbstractUserPreferences { borderRadius: BorderRadius.circular(15), ), leading: UserPreferencesListTile.getTintedIcon(leading, context), - trailing: myCount == null - ? null - : FutureBuilder( - future: myCount, - builder: - (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.connectionState != ConnectionState.done) { - return const SizedBox( - height: LARGE_SPACE, - width: LARGE_SPACE, - child: CircularProgressIndicator.adaptive()); - } - return snapshot.data == null - ? EMPTY_WIDGET - : Text(snapshot.data.toString()); - }, - ), + trailing: + lazyCounter == null ? null : LazyCounterWidget(lazyCounter), ), ), );