diff --git a/.github/labeler.yml b/.github/labeler.yml index 8aa4e9d1757..4f55e0c117d 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -240,7 +240,7 @@ User lists: Product scan carousel: - changed-files: - - any-glob-to-any-file: 'packages/smooth_app/lib/widgets/smooth_product_carousel.dart' + - any-glob-to-any-file: 'packages/smooth_app/lib/widgets/scan_carousel.dart' ✏️ Editing - 📦 Packaging input: - changed-files: diff --git a/packages/smooth_app/lib/data_models/tagline/tagline_json.dart b/packages/smooth_app/lib/data_models/news_feed/newsfeed_json.dart similarity index 94% rename from packages/smooth_app/lib/data_models/tagline/tagline_json.dart rename to packages/smooth_app/lib/data_models/news_feed/newsfeed_json.dart index 4fced2ae96f..9ddd6e2a7ab 100644 --- a/packages/smooth_app/lib/data_models/tagline/tagline_json.dart +++ b/packages/smooth_app/lib/data_models/news_feed/newsfeed_json.dart @@ -1,6 +1,6 @@ -part of 'tagline_provider.dart'; +part of 'newsfeed_provider.dart'; -/// Content from the JSON and converted to what's in "tagmodel.dart" +/// Content from the JSON and converted to what's in "newsfeed_model.dart" class _TagLineJSON { _TagLineJSON.fromJson(Map json) @@ -15,17 +15,16 @@ class _TagLineJSON { final _TagLineJSONNewsList news; final _TaglineJSONFeed taglineFeed; - TagLine toTagLine(String locale) { - final Map tagLineNews = news.map( - (String key, _TagLineItemNewsItem value) => - MapEntry( + AppNews toTagLine(String locale) { + final Map tagLineNews = news.map( + (String key, _TagLineItemNewsItem value) => MapEntry( key, value.toTagLineItem(locale), ), ); final _TagLineJSONFeedLocale localizedFeed = taglineFeed.loadNews(locale); - final Iterable feed = localizedFeed.news + final Iterable feed = localizedFeed.news .map((_TagLineJSONFeedLocaleItem item) { if (news[item.id] == null) { // The asked ID doesn't exist in the news @@ -33,16 +32,16 @@ class _TagLineJSON { } return item.overrideNewsItem(news[item.id]!, locale); }) - .where((TagLineFeedItem? item) => + .where((AppNewsFeedItem? item) => item != null && (item.startDate == null || item.startDate!.isBefore(DateTime.now())) && (item.endDate == null || item.endDate!.isAfter(DateTime.now()))) .whereNotNull(); - return TagLine( - news: TagLineNewsList(tagLineNews), - feed: TagLineFeed( + return AppNews( + news: AppNewsList(tagLineNews), + feed: AppNewsFeed( feed.toList(growable: false), ), ); @@ -106,10 +105,10 @@ class _TagLineItemNewsItem { return _translations['default']!.merge(translation); } - TagLineNewsItem toTagLineItem(String locale) { + AppNewsItem toTagLineItem(String locale) { final _TagLineItemNewsTranslation translation = loadTranslation(locale); // We can assume the default translation has a non-null title and message - return TagLineNewsItem( + return AppNewsItem( id: id, title: translation.title!, message: translation.message!, @@ -224,8 +223,8 @@ class _TagLineNewsImage { final double? width; final String? alt; - TagLineImage toTagLineImage() { - return TagLineImage( + AppNewsImage toTagLineImage() { + return AppNewsImage( src: url, width: width, alt: alt, @@ -303,7 +302,7 @@ class _TagLineNewsStyle { ); } - TagLineStyle toTagLineStyle() => TagLineStyle.fromHexa( + AppNewsStyle toTagLineStyle() => AppNewsStyle.fromHexa( titleBackground: titleBackground, titleTextColor: titleTextColor, titleIndicatorColor: titleIndicatorColor, @@ -369,7 +368,7 @@ class _TagLineJSONFeedLocaleItem { final String id; final _TagLineJSONFeedNewsItemOverride? overrideContent; - TagLineFeedItem overrideNewsItem( + AppNewsFeedItem overrideNewsItem( _TagLineItemNewsItem newsItem, String locale, ) { @@ -384,9 +383,9 @@ class _TagLineJSONFeedLocaleItem { ); } - final TagLineNewsItem tagLineItem = item.toTagLineItem(locale); + final AppNewsItem tagLineItem = item.toTagLineItem(locale); - return TagLineFeedItem( + return AppNewsFeedItem( news: tagLineItem, startDate: tagLineItem.startDate, endDate: tagLineItem.endDate, diff --git a/packages/smooth_app/lib/data_models/tagline/tagline_model.dart b/packages/smooth_app/lib/data_models/news_feed/newsfeed_model.dart similarity index 72% rename from packages/smooth_app/lib/data_models/tagline/tagline_model.dart rename to packages/smooth_app/lib/data_models/news_feed/newsfeed_model.dart index 20698d5713d..59226e46cab 100644 --- a/packages/smooth_app/lib/data_models/tagline/tagline_model.dart +++ b/packages/smooth_app/lib/data_models/news_feed/newsfeed_model.dart @@ -1,35 +1,35 @@ import 'dart:ui'; -class TagLine { - const TagLine({ +class AppNews { + const AppNews({ required this.news, required this.feed, }); - final TagLineNewsList news; - final TagLineFeed feed; + final AppNewsList news; + final AppNewsFeed feed; @override String toString() { - return 'TagLine{news: $news, feed: $feed}'; + return 'AppNews{news: $news, feed: $feed}'; } } -class TagLineNewsList { - const TagLineNewsList(Map news) : _news = news; +class AppNewsList { + const AppNewsList(Map news) : _news = news; - final Map _news; + final Map _news; - TagLineNewsItem? operator [](String key) => _news[key]; + AppNewsItem? operator [](String key) => _news[key]; @override String toString() { - return 'TagLineNewsList{_news: $_news}'; + return 'AppNewsList{_news: $_news}'; } } -class TagLineNewsItem { - const TagLineNewsItem({ +class AppNewsItem { + const AppNewsItem({ required this.id, required this.title, required this.message, @@ -48,17 +48,17 @@ class TagLineNewsItem { final String? buttonLabel; final DateTime? startDate; final DateTime? endDate; - final TagLineImage? image; - final TagLineStyle? style; + final AppNewsImage? image; + final AppNewsStyle? style; @override String toString() { - return 'TagLineNewsItem{id: $id, title: $title, message: $message, url: $url, buttonLabel: $buttonLabel, startDate: $startDate, endDate: $endDate, image: $image, style: $style}'; + return 'AppNewsItem{id: $id, title: $title, message: $message, url: $url, buttonLabel: $buttonLabel, startDate: $startDate, endDate: $endDate, image: $image, style: $style}'; } } -class TagLineStyle { - const TagLineStyle({ +class AppNewsStyle { + const AppNewsStyle({ this.titleBackground, this.titleTextColor, this.titleIndicatorColor, @@ -69,7 +69,7 @@ class TagLineStyle { this.contentBackgroundColor, }); - TagLineStyle.fromHexa({ + AppNewsStyle.fromHexa({ String? titleBackground, String? titleTextColor, String? titleIndicatorColor, @@ -105,12 +105,12 @@ class TagLineStyle { @override String toString() { - return 'TagLineStyle{titleBackground: $titleBackground, titleTextColor: $titleTextColor, titleIndicatorColor: $titleIndicatorColor, messageBackground: $messageBackground, messageTextColor: $messageTextColor, buttonBackground: $buttonBackground, buttonTextColor: $buttonTextColor, contentBackgroundColor: $contentBackgroundColor}'; + return 'AppNewsStyle{titleBackground: $titleBackground, titleTextColor: $titleTextColor, titleIndicatorColor: $titleIndicatorColor, messageBackground: $messageBackground, messageTextColor: $messageTextColor, buttonBackground: $buttonBackground, buttonTextColor: $buttonTextColor, contentBackgroundColor: $contentBackgroundColor}'; } } -class TagLineImage { - const TagLineImage({ +class AppNewsImage { + const AppNewsImage({ required this.src, this.width, this.alt, @@ -122,14 +122,14 @@ class TagLineImage { @override String toString() { - return 'TagLineImage{src: $src, width: $width, alt: $alt}'; + return 'AppNewsImage{src: $src, width: $width, alt: $alt}'; } } -class TagLineFeed { - const TagLineFeed(this.news); +class AppNewsFeed { + const AppNewsFeed(this.news); - final List news; + final List news; bool get isNotEmpty => news.isNotEmpty; @@ -139,15 +139,15 @@ class TagLineFeed { } } -class TagLineFeedItem { - const TagLineFeedItem({ +class AppNewsFeedItem { + const AppNewsFeedItem({ required this.news, DateTime? startDate, DateTime? endDate, }) : _startDate = startDate, _endDate = endDate; - final TagLineNewsItem news; + final AppNewsItem news; final DateTime? _startDate; final DateTime? _endDate; @@ -159,6 +159,6 @@ class TagLineFeedItem { @override String toString() { - return 'TagLineFeedItem{news: $news, _startDate: $_startDate, _endDate: $_endDate}'; + return 'AppNewsFeedItem{news: $news, _startDate: $_startDate, _endDate: $_endDate}'; } } diff --git a/packages/smooth_app/lib/data_models/tagline/tagline_provider.dart b/packages/smooth_app/lib/data_models/news_feed/newsfeed_provider.dart similarity index 67% rename from packages/smooth_app/lib/data_models/tagline/tagline_provider.dart rename to packages/smooth_app/lib/data_models/news_feed/newsfeed_provider.dart index 6489cb8ab54..f5b8e8619ab 100644 --- a/packages/smooth_app/lib/data_models/tagline/tagline_provider.dart +++ b/packages/smooth_app/lib/data_models/news_feed/newsfeed_provider.dart @@ -8,23 +8,23 @@ import 'package:http/http.dart' as http; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:smooth_app/data_models/news_feed/newsfeed_model.dart'; import 'package:smooth_app/data_models/preferences/user_preferences.dart'; -import 'package:smooth_app/data_models/tagline/tagline_model.dart'; import 'package:smooth_app/pages/preferences/user_preferences_dev_mode.dart'; import 'package:smooth_app/query/product_query.dart'; import 'package:smooth_app/services/smooth_services.dart'; -part 'tagline_json.dart'; +part 'newsfeed_json.dart'; -/// The TagLine provides one one side a list of news and on the other a feed -/// containing the some of the news +/// This provides one one side a list of news and on the other a feed of news. +/// A feed contains some of the news? /// -/// The TagLine is fetched on the server and cached locally (1 day). +/// The content is fetched on the server and cached locally (1 day). /// To be notified of changes, listen to this [ChangeNotifier] and more -/// particularly to the [state] property -class TagLineProvider extends ChangeNotifier { - TagLineProvider(UserPreferences preferences) - : _state = const TagLineLoading(), +/// particularly to the [state] property. +class AppNewsProvider extends ChangeNotifier { + AppNewsProvider(UserPreferences preferences) + : _state = const AppNewsStateLoading(), _preferences = preferences, _domain = preferences.getDevModeString( UserPreferencesDevMode.userPreferencesTestEnvDomain) ?? @@ -33,17 +33,17 @@ class TagLineProvider extends ChangeNotifier { .getFlag(UserPreferencesDevMode.userPreferencesFlagProd) ?? true { _preferences.addListener(_onPreferencesChanged); - loadTagLine(); + loadLatestNews(); } final UserPreferences _preferences; - TagLineState _state; + AppNewsState _state; - bool get hasContent => _state is TagLineLoaded; + bool get hasContent => _state is AppNewsStateLoaded; - Future loadTagLine({bool forceUpdate = false}) async { - _emit(const TagLineLoading()); + Future loadLatestNews({bool forceUpdate = false}) async { + _emit(const AppNewsStateLoading()); final String locale = ProductQuery.getLocaleString(); if (locale.startsWith('-')) { @@ -51,43 +51,43 @@ class TagLineProvider extends ChangeNotifier { return; } - final File cacheFile = await _tagLineCacheFile; + final File cacheFile = await _newsCacheFile; String? jsonString; // Try from the cache first - if (!forceUpdate && _isTagLineCacheValid(cacheFile)) { + if (!forceUpdate && _isNewsCacheValid(cacheFile)) { jsonString = cacheFile.readAsStringSync(); } if (jsonString == null || jsonString.isEmpty == true) { - jsonString = await _fetchTagLine(); + jsonString = await _fetchJSON(); } if (jsonString?.isNotEmpty != true) { - _emit(const TagLineError('JSON file is empty')); + _emit(const AppNewsStateError('JSON news file is empty')); return; } - final TagLine? tagLine = await Isolate.run( + final AppNews? tagLine = await Isolate.run( () => _parseJSONAndGetLocalizedContent(jsonString!, locale)); if (tagLine == null) { - _emit(const TagLineError('Unable to parse the JSON file')); - Logs.e('Unable to parse the Tagline file'); + _emit(const AppNewsStateError('Unable to parse the JSON news file')); + Logs.e('Unable to parse the JSON news file'); } else { - _emit(TagLineLoaded(tagLine)); - Logs.i('TagLine reloaded'); + _emit(AppNewsStateLoaded(tagLine)); + Logs.i('News ${forceUpdate ? 're' : ''}loaded'); } } - void _emit(TagLineState state) { + void _emit(AppNewsState state) { _state = state; WidgetsBinding.instance.addPostFrameCallback((_) { notifyListeners(); }); } - TagLineState get state => _state; + AppNewsState get state => _state; - static Future _parseJSONAndGetLocalizedContent( + static Future _parseJSONAndGetLocalizedContent( String json, String locale, ) async { @@ -102,11 +102,11 @@ class TagLineProvider extends ChangeNotifier { /// API URL: [https://world.openfoodfacts.[org/net]/resources/files/tagline-off-ios-v3.json] /// or [https://world.openfoodfacts.[org/net]/resources/files/tagline-off-android-v3.json] - Future _fetchTagLine() async { + Future _fetchJSON() async { try { final UriProductHelper uriProductHelper = ProductQuery.uriProductHelper; final Map headers = {}; - final Uri uri = uriProductHelper.getUri(path: _tagLineUrl); + final Uri uri = uriProductHelper.getUri(path: _newsUrl); if (uriProductHelper.userInfoForPatch != null) { headers['Authorization'] = @@ -125,7 +125,7 @@ class TagLineProvider extends ChangeNotifier { if (!json.startsWith('[') && !json.startsWith('{')) { throw Exception('Invalid JSON'); } - await _saveTagLineToCache(json); + await _saveNewsToCache(json); return json; } catch (_) { return null; @@ -133,7 +133,7 @@ class TagLineProvider extends ChangeNotifier { } /// Based on the platform, the URL may differ - String get _tagLineUrl { + String get _newsUrl { if (Platform.isIOS || Platform.isMacOS) { return '/resources/files/tagline-off-ios-v3.json'; } else { @@ -141,15 +141,15 @@ class TagLineProvider extends ChangeNotifier { } } - Future get _tagLineCacheFile => getApplicationCacheDirectory() + Future get _newsCacheFile => getApplicationCacheDirectory() .then((Directory dir) => File(join(dir.path, 'tagline.json'))); - Future _saveTagLineToCache(final String json) async { - final File file = await _tagLineCacheFile; + Future _saveNewsToCache(final String json) async { + final File file = await _newsCacheFile; return file.writeAsString(json); } - bool _isTagLineCacheValid(File file) => + bool _isNewsCacheValid(File file) => file.existsSync() && file.lengthSync() > 0 && file @@ -172,7 +172,7 @@ class TagLineProvider extends ChangeNotifier { if (domain != _domain || prodEnv != _prodEnv) { _domain = domain; _prodEnv = prodEnv; - loadTagLine(forceUpdate: true); + loadLatestNews(forceUpdate: true); } } @@ -183,22 +183,22 @@ class TagLineProvider extends ChangeNotifier { } } -sealed class TagLineState { - const TagLineState(); +sealed class AppNewsState { + const AppNewsState(); } -final class TagLineLoading extends TagLineState { - const TagLineLoading(); +final class AppNewsStateLoading extends AppNewsState { + const AppNewsStateLoading(); } -class TagLineLoaded extends TagLineState { - const TagLineLoaded(this.tagLineContent); +class AppNewsStateLoaded extends AppNewsState { + const AppNewsStateLoaded(this.tagLineContent); - final TagLine tagLineContent; + final AppNews tagLineContent; } -class TagLineError extends TagLineState { - const TagLineError(this.exception); +class AppNewsStateError extends AppNewsState { + const AppNewsStateError(this.exception); final dynamic exception; } diff --git a/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_page.dart b/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_page.dart index 4d629568009..93eb004619a 100644 --- a/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_page.dart +++ b/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_page.dart @@ -10,8 +10,8 @@ import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_card.dart'; import 'package:smooth_app/knowledge_panel/knowledge_panels/knowledge_panel_expanded_card.dart'; import 'package:smooth_app/knowledge_panel/knowledge_panels_builder.dart'; -import 'package:smooth_app/pages/carousel_manager.dart'; import 'package:smooth_app/pages/product/common/product_refresher.dart'; +import 'package:smooth_app/pages/scan/carousel/scan_carousel_manager.dart'; import 'package:smooth_app/widgets/smooth_app_bar.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; @@ -103,7 +103,7 @@ class _KnowledgePanelPageState extends State Future _refreshProduct(BuildContext context) async { try { final String? barcode = - ExternalCarouselManager.read(context).currentBarcode; + ExternalScanCarouselManager.read(context).currentBarcode; if (barcode?.isEmpty == true) { return; } diff --git a/packages/smooth_app/lib/main.dart b/packages/smooth_app/lib/main.dart index 79d451e26f8..5984adaf0c7 100644 --- a/packages/smooth_app/lib/main.dart +++ b/packages/smooth_app/lib/main.dart @@ -16,9 +16,9 @@ import 'package:provider/single_child_widget.dart'; import 'package:scanner_shared/scanner_shared.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:smooth_app/data_models/continuous_scan_model.dart'; +import 'package:smooth_app/data_models/news_feed/newsfeed_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/data_models/tagline/tagline_provider.dart'; import 'package:smooth_app/data_models/user_management_provider.dart'; import 'package:smooth_app/database/dao_string.dart'; import 'package:smooth_app/database/local_database.dart'; @@ -224,8 +224,8 @@ class _SmoothAppState extends State { provide(_continuousScanModel), provide(_permissionListener), ], - child: ChangeNotifierProvider( - create: (BuildContext context) => TagLineProvider( + child: ChangeNotifierProvider( + create: (BuildContext context) => AppNewsProvider( context.read(), ), lazy: true, diff --git a/packages/smooth_app/lib/pages/navigator/app_navigator.dart b/packages/smooth_app/lib/pages/navigator/app_navigator.dart index 02c951306d0..68f33560b2b 100644 --- a/packages/smooth_app/lib/pages/navigator/app_navigator.dart +++ b/packages/smooth_app/lib/pages/navigator/app_navigator.dart @@ -3,12 +3,11 @@ import 'package:flutter/widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:provider/provider.dart'; +import 'package:smooth_app/data_models/news_feed/newsfeed_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/data_models/tagline/tagline_provider.dart'; import 'package:smooth_app/helpers/analytics_helper.dart'; import 'package:smooth_app/helpers/extension_on_text_helper.dart'; -import 'package:smooth_app/pages/carousel_manager.dart'; import 'package:smooth_app/pages/guides/guide/guide_nutriscore_v2.dart'; import 'package:smooth_app/pages/navigator/error_page.dart'; import 'package:smooth_app/pages/navigator/external_page.dart'; @@ -18,6 +17,7 @@ import 'package:smooth_app/pages/product/add_new_product_page.dart'; import 'package:smooth_app/pages/product/edit_product_page.dart'; import 'package:smooth_app/pages/product/new_product_page.dart'; import 'package:smooth_app/pages/product/product_loader_page.dart'; +import 'package:smooth_app/pages/scan/carousel/scan_carousel_manager.dart'; import 'package:smooth_app/pages/scan/search_page.dart'; import 'package:smooth_app/pages/scan/search_product_helper.dart'; import 'package:smooth_app/pages/user_management/sign_up_page.dart'; @@ -145,8 +145,8 @@ class _SmoothGoRouter { heroTag: state.uri.queryParameters['heroTag'], ); - if (ExternalCarouselManager.find(context) == null) { - return ExternalCarouselManager(child: widget); + if (ExternalScanCarouselManager.find(context) == null) { + return ExternalScanCarouselManager(child: widget); } else { return widget; } @@ -313,7 +313,7 @@ class _SmoothGoRouter { // Must be set first to ensure the method is only called once _appLanguageInitialized = true; ProductQuery.setLanguage(context, context.read()); - context.read().loadTagLine(); + context.read().loadLatestNews(); return context.read().refresh(); } diff --git a/packages/smooth_app/lib/pages/onboarding/onboarding_flow_navigator.dart b/packages/smooth_app/lib/pages/onboarding/onboarding_flow_navigator.dart index 9cd670c5ee9..06ad23910ac 100644 --- a/packages/smooth_app/lib/pages/onboarding/onboarding_flow_navigator.dart +++ b/packages/smooth_app/lib/pages/onboarding/onboarding_flow_navigator.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:smooth_app/data_models/preferences/user_preferences.dart'; import 'package:smooth_app/database/local_database.dart'; -import 'package:smooth_app/pages/carousel_manager.dart'; import 'package:smooth_app/pages/navigator/app_navigator.dart'; import 'package:smooth_app/pages/onboarding/consent_analytics_page.dart'; import 'package:smooth_app/pages/onboarding/permissions_page.dart'; @@ -13,6 +12,7 @@ import 'package:smooth_app/pages/onboarding/sample_health_card_page.dart'; import 'package:smooth_app/pages/onboarding/scan_example.dart'; import 'package:smooth_app/pages/onboarding/welcome_page.dart'; import 'package:smooth_app/pages/page_manager.dart'; +import 'package:smooth_app/pages/scan/carousel/scan_carousel_manager.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; import 'package:smooth_app/widgets/will_pop_scope.dart'; @@ -114,7 +114,7 @@ enum OnboardingPage { ConsentAnalyticsPage(backgroundColor), ); case OnboardingPage.ONBOARDING_COMPLETE: - return ExternalCarouselManager(child: PageManager()); + return ExternalScanCarouselManager(child: PageManager()); } } diff --git a/packages/smooth_app/lib/pages/page_manager.dart b/packages/smooth_app/lib/pages/page_manager.dart index 615ee2500fb..5ef62fcfec1 100644 --- a/packages/smooth_app/lib/pages/page_manager.dart +++ b/packages/smooth_app/lib/pages/page_manager.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; import 'package:smooth_app/data_models/preferences/user_preferences.dart'; -import 'package:smooth_app/pages/carousel_manager.dart'; import 'package:smooth_app/pages/preferences/user_preferences_dev_mode.dart'; +import 'package:smooth_app/pages/scan/carousel/scan_carousel_manager.dart'; import 'package:smooth_app/widgets/tab_navigator.dart'; import 'package:smooth_app/widgets/will_pop_scope.dart'; @@ -62,8 +62,8 @@ class PageManagerState extends State { @override Widget build(BuildContext context) { final AppLocalizations appLocalizations = AppLocalizations.of(context); - final ExternalCarouselManagerState carouselManager = - ExternalCarouselManager.watch(context); + final ExternalScanCarouselManagerState carouselManager = + ExternalScanCarouselManager.watch(context); if (carouselManager.forceShowScannerTab) { _currentPage = BottomNavigationTab.Scan; 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 756706ee82b..6882d43bdbf 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,9 +3,9 @@ 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_language_refresh.dart'; +import 'package:smooth_app/data_models/news_feed/newsfeed_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/data_models/tagline/tagline_provider.dart'; import 'package:smooth_app/database/local_database.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/widgets/language_selector.dart'; @@ -57,9 +57,9 @@ class UserPreferencesLanguageSelector extends StatelessWidget { context.read(), ); - // Refresh the tagline + // Refresh the news feed if (context.mounted) { - context.read().loadTagLine(); + context.read().loadLatestNews(); } // TODO(monsieurtanuki): make it a background task also? // no await 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 87cd95f4910..d831b6c70bc 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 @@ -21,7 +21,6 @@ import 'package:smooth_app/generic_lib/widgets/smooth_responsive.dart'; import 'package:smooth_app/helpers/app_helper.dart'; import 'package:smooth_app/helpers/robotoff_insight_helper.dart'; import 'package:smooth_app/pages/all_product_list_modal.dart'; -import 'package:smooth_app/pages/carousel_manager.dart'; import 'package:smooth_app/pages/preferences/user_preferences_dev_mode.dart'; import 'package:smooth_app/pages/product/common/product_list_item_popup_items.dart'; import 'package:smooth_app/pages/product/common/product_list_item_simple.dart'; @@ -29,6 +28,7 @@ import 'package:smooth_app/pages/product/common/product_list_popup_items.dart'; import 'package:smooth_app/pages/product/common/product_query_page_helper.dart'; import 'package:smooth_app/pages/product/common/product_refresher.dart'; import 'package:smooth_app/pages/product_list_user_dialog_helper.dart'; +import 'package:smooth_app/pages/scan/carousel/scan_carousel_manager.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'; @@ -133,7 +133,7 @@ class _ProductListPageState extends State icon: const Icon(CupertinoIcons.barcode), label: Text(appLocalizations.product_list_empty_title), onPressed: () => - ExternalCarouselManager.read(context).showSearchCard(), + ExternalScanCarouselManager.read(context).showSearchCard(), ) : _selectionMode ? null diff --git a/packages/smooth_app/lib/pages/product/new_product_page.dart b/packages/smooth_app/lib/pages/product/new_product_page.dart index d5e70156a3c..5e8e1c5f772 100644 --- a/packages/smooth_app/lib/pages/product/new_product_page.dart +++ b/packages/smooth_app/lib/pages/product/new_product_page.dart @@ -21,7 +21,6 @@ import 'package:smooth_app/generic_lib/duration_constants.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_back_button.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_card.dart'; import 'package:smooth_app/helpers/analytics_helper.dart'; -import 'package:smooth_app/pages/carousel_manager.dart'; import 'package:smooth_app/pages/preferences/user_preferences_dev_mode.dart'; import 'package:smooth_app/pages/prices/prices_card.dart'; import 'package:smooth_app/pages/product/common/product_list_page.dart'; @@ -34,6 +33,7 @@ import 'package:smooth_app/pages/product/standard_knowledge_panel_cards.dart'; import 'package:smooth_app/pages/product/summary_card.dart'; import 'package:smooth_app/pages/product/website_card.dart'; import 'package:smooth_app/pages/product_list_user_dialog_helper.dart'; +import 'package:smooth_app/pages/scan/carousel/scan_carousel_manager.dart'; import 'package:smooth_app/query/product_query.dart'; import 'package:smooth_app/themes/constant_icons.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; @@ -85,8 +85,8 @@ class _ProductPageState extends State @override Widget build(BuildContext context) { - final ExternalCarouselManagerState carouselManager = - ExternalCarouselManager.read(context); + final ExternalScanCarouselManagerState carouselManager = + ExternalScanCarouselManager.read(context); carouselManager.currentBarcode = barcode; final ThemeData themeData = Theme.of(context); _productPreferences = context.watch(); diff --git a/packages/smooth_app/lib/pages/scan/carousel/main_card/scan_main_card.dart b/packages/smooth_app/lib/pages/scan/carousel/main_card/scan_main_card.dart new file mode 100644 index 00000000000..a693aecf8d5 --- /dev/null +++ b/packages/smooth_app/lib/pages/scan/carousel/main_card/scan_main_card.dart @@ -0,0 +1,179 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:provider/provider.dart'; +import 'package:smooth_app/data_models/news_feed/newsfeed_provider.dart'; +import 'package:smooth_app/generic_lib/design_constants.dart'; +import 'package:smooth_app/generic_lib/widgets/smooth_card.dart'; +import 'package:smooth_app/helpers/provider_helper.dart'; +import 'package:smooth_app/helpers/strings_helper.dart'; +import 'package:smooth_app/pages/navigator/app_navigator.dart'; +import 'package:smooth_app/pages/scan/carousel/main_card/scan_tagline.dart'; +import 'package:smooth_app/resources/app_icons.dart'; +import 'package:smooth_app/themes/smooth_theme_colors.dart'; +import 'package:smooth_app/themes/theme_provider.dart'; + +class ScanMainCard extends StatelessWidget { + const ScanMainCard(); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Expanded( + child: ConsumerFilter( + buildWhen: + (AppNewsProvider? previousValue, AppNewsProvider currentValue) { + return previousValue?.hasContent != currentValue.hasContent; + }, + builder: (BuildContext context, AppNewsProvider newsFeed, _) { + if (!newsFeed.hasContent) { + return const _SearchCard( + expandedMode: true, + ); + } else { + return const Column( + children: [ + Expanded( + flex: 6, + child: _SearchCard( + expandedMode: false, + ), + ), + SizedBox(height: MEDIUM_SPACE), + Expanded( + flex: 4, + child: ScanTagLine(), + ), + ], + ); + } + }, + ), + ), + ], + ); + } +} + +class _SearchCard extends StatelessWidget { + const _SearchCard({ + required this.expandedMode, + }); + + /// Expanded is when this card is the only one (no tagline, no app review…) + final bool expandedMode; + + @override + Widget build(BuildContext context) { + final AppLocalizations localizations = AppLocalizations.of(context); + final bool lightTheme = !context.watch().isDarkMode(context); + + final Widget widget = SmoothCard( + color: lightTheme ? Colors.grey.withOpacity(0.1) : Colors.black, + padding: const EdgeInsets.symmetric( + vertical: MEDIUM_SPACE, + horizontal: LARGE_SPACE, + ), + margin: const EdgeInsets.symmetric( + horizontal: 0.0, + vertical: VERY_SMALL_SPACE, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + SvgPicture.asset( + lightTheme + ? 'assets/app/logo_text_black.svg' + : 'assets/app/logo_text_white.svg', + semanticsLabel: localizations.homepage_main_card_logo_description, + ), + FormattedText( + text: localizations.homepage_main_card_subheading, + textAlign: TextAlign.center, + textStyle: const TextStyle(height: 1.3), + ), + const _SearchBar(), + ], + ), + ); + + if (expandedMode) { + return ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.sizeOf(context).height * 0.4, + ), + child: widget, + ); + } else { + return widget; + } + } +} + +class _SearchBar extends StatelessWidget { + const _SearchBar(); + + static const double SEARCH_BAR_HEIGHT = 47.0; + + @override + Widget build(BuildContext context) { + final AppLocalizations localizations = AppLocalizations.of(context); + final SmoothColorsThemeExtension theme = + Theme.of(context).extension()!; + final bool lightTheme = !context.watch().isDarkMode(context); + + return SizedBox( + height: SEARCH_BAR_HEIGHT, + child: InkWell( + onTap: () => AppNavigator.of(context).push(AppRoutes.SEARCH), + borderRadius: BorderRadius.circular(30.0), + child: Ink( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30.0), + color: lightTheme ? Colors.white : theme.greyDark, + border: Border.all(color: theme.primaryBlack), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsetsDirectional.only( + start: 20.0, + end: 10.0, + bottom: 3.0, + ), + child: Text( + localizations.homepage_main_card_search_field_hint, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: lightTheme ? Colors.black : Colors.white, + ), + ), + ), + ), + AspectRatio( + aspectRatio: 1.0, + child: DecoratedBox( + decoration: BoxDecoration( + color: theme.primaryDark, + shape: BoxShape.circle, + ), + child: const Padding( + padding: EdgeInsets.all(10.0), + child: Search( + size: 20.0, + color: Colors.white, + ), + ), + ), + ) + ], + ), + ), + ), + ); + } +} diff --git a/packages/smooth_app/lib/pages/scan/scan_tagline.dart b/packages/smooth_app/lib/pages/scan/carousel/main_card/scan_tagline.dart similarity index 87% rename from packages/smooth_app/lib/pages/scan/scan_tagline.dart rename to packages/smooth_app/lib/pages/scan/carousel/main_card/scan_tagline.dart index 8fabda2f8a8..cbd3869f349 100644 --- a/packages/smooth_app/lib/pages/scan/scan_tagline.dart +++ b/packages/smooth_app/lib/pages/scan/carousel/main_card/scan_tagline.dart @@ -5,9 +5,9 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; import 'package:shimmer/shimmer.dart'; import 'package:smooth_app/cards/category_cards/svg_cache.dart'; +import 'package:smooth_app/data_models/news_feed/newsfeed_model.dart'; +import 'package:smooth_app/data_models/news_feed/newsfeed_provider.dart'; import 'package:smooth_app/data_models/preferences/user_preferences.dart'; -import 'package:smooth_app/data_models/tagline/tagline_model.dart'; -import 'package:smooth_app/data_models/tagline/tagline_provider.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_card.dart'; import 'package:smooth_app/helpers/launch_url_helper.dart'; @@ -22,12 +22,12 @@ class ScanTagLine extends StatelessWidget { @override Widget build(BuildContext context) { - return ChangeNotifierProvider<_ScanTagLineProvider>( - create: (BuildContext context) => _ScanTagLineProvider(context), - child: Consumer<_ScanTagLineProvider>( + return ChangeNotifierProvider<_ScanNewsFeedProvider>( + create: (BuildContext context) => _ScanNewsFeedProvider(context), + child: Consumer<_ScanNewsFeedProvider>( builder: ( BuildContext context, - _ScanTagLineProvider scanTagLineProvider, + _ScanNewsFeedProvider scanTagLineProvider, Widget? child, ) { final _ScanTagLineState state = scanTagLineProvider.value; @@ -70,7 +70,7 @@ class _ScanTagLineContent extends StatefulWidget { required this.news, }); - final Iterable news; + final Iterable news; @override State<_ScanTagLineContent> createState() => _ScanTagLineContentState(); @@ -102,7 +102,7 @@ class _ScanTagLineContentState extends State<_ScanTagLineContent> { final ThemeProvider themeProvider = context.watch(); final SmoothColorsThemeExtension theme = Theme.of(context).extension()!; - final TagLineNewsItem currentNews = widget.news.elementAt(_index); + final AppNewsItem currentNews = widget.news.elementAt(_index); // Default values seem weird const Radius radius = Radius.circular(16.0); @@ -241,7 +241,7 @@ class _TagLineContentBody extends StatelessWidget { final String message; final Color? textColor; - final TagLineImage? image; + final AppNewsImage? image; @override Widget build(BuildContext context) { @@ -346,49 +346,49 @@ class _TagLineContentButton extends StatelessWidget { } } -/// Listen to [TagLineProvider] feed and provide a list of [TagLineNewsItem] +/// Listen to [AppNewsProvider] feed and provide a list of [AppNewsItem] /// randomly sorted by unread, then displayed and clicked news. -class _ScanTagLineProvider extends ValueNotifier<_ScanTagLineState> { - _ScanTagLineProvider(BuildContext context) - : _tagLineProvider = context.read(), +class _ScanNewsFeedProvider extends ValueNotifier<_ScanTagLineState> { + _ScanNewsFeedProvider(BuildContext context) + : _newsFeedProvider = context.read(), _userPreferences = context.read(), super(const _ScanTagLineStateLoading()) { - _tagLineProvider.addListener(_onTagLineStateChanged); + _newsFeedProvider.addListener(_onNewsFeedStateChanged); // Refresh with the current state - _onTagLineStateChanged(); + _onNewsFeedStateChanged(); } - final TagLineProvider _tagLineProvider; + final AppNewsProvider _newsFeedProvider; final UserPreferences _userPreferences; - void _onTagLineStateChanged() { - switch (_tagLineProvider.state) { - case TagLineLoading(): + void _onNewsFeedStateChanged() { + switch (_newsFeedProvider.state) { + case AppNewsStateLoading(): emit(const _ScanTagLineStateLoading()); - case TagLineError(): + case AppNewsStateError(): emit(const _ScanTagLineStateNoContent()); - case TagLineLoaded(): + case AppNewsStateLoaded(): _onTagLineContentAvailable( - (_tagLineProvider.state as TagLineLoaded).tagLineContent); + (_newsFeedProvider.state as AppNewsStateLoaded).tagLineContent); } } - Future _onTagLineContentAvailable(TagLine tagLine) async { + Future _onTagLineContentAvailable(AppNews tagLine) async { if (!tagLine.feed.isNotEmpty) { emit(const _ScanTagLineStateNoContent()); return; } - final List unreadNews = []; - final List displayedNews = []; - final List clickedNews = []; + final List unreadNews = []; + final List displayedNews = []; + final List clickedNews = []; final List taglineFeedAlreadyClickedNews = _userPreferences.taglineFeedClickedNews; final List taglineFeedAlreadyDisplayedNews = _userPreferences.taglineFeedDisplayedNews; - for (final TagLineFeedItem feedItem in tagLine.feed.news) { + for (final AppNewsFeedItem feedItem in tagLine.feed.news) { if (taglineFeedAlreadyClickedNews.contains(feedItem.id)) { clickedNews.add(feedItem.news); } else if (taglineFeedAlreadyDisplayedNews.contains(feedItem.id)) { @@ -400,7 +400,7 @@ class _ScanTagLineProvider extends ValueNotifier<_ScanTagLineState> { emit( _ScanTagLineStateLoaded( - [ + [ ...unreadNews..shuffle(), ...displayedNews..shuffle(), ...clickedNews..shuffle(), @@ -411,7 +411,7 @@ class _ScanTagLineProvider extends ValueNotifier<_ScanTagLineState> { @override void dispose() { - _tagLineProvider.removeListener(_onTagLineStateChanged); + _newsFeedProvider.removeListener(_onNewsFeedStateChanged); super.dispose(); } } @@ -431,5 +431,5 @@ class _ScanTagLineStateNoContent extends _ScanTagLineState { class _ScanTagLineStateLoaded extends _ScanTagLineState { const _ScanTagLineStateLoaded(this.tagLine); - final Iterable tagLine; + final Iterable tagLine; } diff --git a/packages/smooth_app/lib/pages/scan/carousel/scan_carousel.dart b/packages/smooth_app/lib/pages/scan/carousel/scan_carousel.dart new file mode 100644 index 00000000000..6ab2e199366 --- /dev/null +++ b/packages/smooth_app/lib/pages/scan/carousel/scan_carousel.dart @@ -0,0 +1,195 @@ +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:scanner_shared/scanner_shared.dart' hide EMPTY_WIDGET; +import 'package:smooth_app/cards/product_cards/smooth_product_card_error.dart'; +import 'package:smooth_app/cards/product_cards/smooth_product_card_loading.dart'; +import 'package:smooth_app/cards/product_cards/smooth_product_card_not_found.dart'; +import 'package:smooth_app/cards/product_cards/smooth_product_card_thanks.dart'; +import 'package:smooth_app/data_models/continuous_scan_model.dart'; +import 'package:smooth_app/generic_lib/design_constants.dart'; +import 'package:smooth_app/pages/scan/carousel/main_card/scan_main_card.dart'; +import 'package:smooth_app/pages/scan/carousel/scan_carousel_manager.dart'; +import 'package:smooth_app/pages/scan/scan_product_card_loader.dart'; + +class ScanPageCarousel extends StatefulWidget { + const ScanPageCarousel({ + this.onPageChangedTo, + }); + + final Function(int page, String? productBarcode)? onPageChangedTo; + + @override + State createState() => _ScanPageCarouselState(); +} + +class _ScanPageCarouselState extends State { + static const double HORIZONTAL_SPACE_BETWEEN_CARDS = 5.0; + + List barcodes = []; + String? _lastConsultedBarcode; + int? _carrouselMovingTo; + int _lastIndex = 0; + + late ContinuousScanModel _model; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _model = context.watch(); + + if (!ExternalScanCarouselManager.read(context).controller.ready) { + return; + } + + barcodes = _model.getBarcodes(); + + if (barcodes.isEmpty) { + // Ensure to reset all variables + _lastConsultedBarcode = null; + _carrouselMovingTo = null; + _lastIndex = 0; + return; + } else if (_lastConsultedBarcode == _model.latestConsultedBarcode) { + // Prevent multiple irrelevant movements + return; + } + + _lastConsultedBarcode = _model.latestConsultedBarcode; + final int cardsCount = barcodes.length + 1; + + if (_model.latestConsultedBarcode != null && + _model.latestConsultedBarcode!.isNotEmpty) { + final int indexBarcode = barcodes.indexOf(_model.latestConsultedBarcode!); + if (indexBarcode >= 0) { + final int indexCarousel = indexBarcode + 1; + _moveControllerTo(indexCarousel); + } else { + if (_lastIndex > cardsCount) { + _moveControllerTo(cardsCount); + } else { + _moveControllerTo(_lastIndex); + } + } + } else { + _moveControllerTo(0); + } + } + + Future _moveControllerTo(int page) async { + if (_carrouselMovingTo == null && _lastIndex != page) { + widget.onPageChangedTo?.call( + page, + page >= 1 ? barcodes[page - 1] : null, + ); + + _carrouselMovingTo = page; + ExternalScanCarouselManager.read(context).animatePageTo(page); + _carrouselMovingTo = null; + } + } + + @override + Widget build(BuildContext context) { + barcodes = _model.getBarcodes(); + + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return CarouselSlider.builder( + itemCount: barcodes.length + 1, + itemBuilder: + (BuildContext context, int itemIndex, int itemRealIndex) { + return SizedBox.expand( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: HORIZONTAL_SPACE_BETWEEN_CARDS, + ), + child: itemIndex == 0 + ? const ScanMainCard() + : _getWidget(itemIndex - 1), + ), + ); + }, + carouselController: + ExternalScanCarouselManager.watch(context).controller, + options: CarouselOptions( + enlargeCenterPage: false, + viewportFraction: _computeViewPortFraction(), + height: constraints.maxHeight, + enableInfiniteScroll: false, + onPageChanged: (int index, CarouselPageChangedReason reason) { + _lastIndex = index; + + if (index > 0) { + if (reason == CarouselPageChangedReason.manual) { + _model.lastConsultedBarcode = barcodes[index - 1]; + _lastConsultedBarcode = _model.latestConsultedBarcode; + } + } else if (index == 0) { + _model.lastConsultedBarcode = null; + _lastConsultedBarcode = null; + } + }, + ), + ); + }, + ); + } + + /// Displays the card for this [index] of a list of [barcodes] + /// + /// There are special cases when the item display is refreshed + /// after the product disappeared and before the whole carousel is refreshed. + /// In those cases, we don't want the app to crash and display a Container + /// instead in the meanwhile. + Widget _getWidget(final int index) { + if (index >= barcodes.length) { + return EMPTY_WIDGET; + } + final String barcode = barcodes[index]; + switch (_model.getBarcodeState(barcode)!) { + case ScannedProductState.FOUND: + case ScannedProductState.CACHED: + return ScanProductCardLoader(barcode); + case ScannedProductState.LOADING: + return SmoothProductCardLoading( + barcode: barcode, + onRemoveProduct: (_) => _model.removeBarcode(barcode), + ); + case ScannedProductState.NOT_FOUND: + return SmoothProductCardNotFound( + barcode: barcode, + onAddProduct: () async { + await _model.refresh(); + setState(() {}); + }, + onRemoveProduct: (_) => _model.removeBarcode(barcode), + ); + case ScannedProductState.THANKS: + return const SmoothProductCardThanks(); + case ScannedProductState.ERROR_INTERNET: + return SmoothProductCardError( + barcode: barcode, + errorType: ScannedProductState.ERROR_INTERNET, + ); + case ScannedProductState.ERROR_INVALID_CODE: + return SmoothProductCardError( + barcode: barcode, + errorType: ScannedProductState.ERROR_INVALID_CODE, + ); + } + } + + double _computeViewPortFraction() { + final double screenWidth = MediaQuery.sizeOf(context).width; + if (barcodes.isEmpty) { + return 0.95; + } + + return (screenWidth - + (SmoothBarcodeScannerVisor.CORNER_PADDING * 2) - + (SmoothBarcodeScannerVisor.STROKE_WIDTH * 2) + + (HORIZONTAL_SPACE_BETWEEN_CARDS * 4)) / + screenWidth; + } +} diff --git a/packages/smooth_app/lib/pages/carousel_manager.dart b/packages/smooth_app/lib/pages/scan/carousel/scan_carousel_manager.dart similarity index 75% rename from packages/smooth_app/lib/pages/carousel_manager.dart rename to packages/smooth_app/lib/pages/scan/carousel/scan_carousel_manager.dart index d3ad42eadf4..02bccf79825 100644 --- a/packages/smooth_app/lib/pages/carousel_manager.dart +++ b/packages/smooth_app/lib/pages/scan/carousel/scan_carousel_manager.dart @@ -2,36 +2,38 @@ import 'package:carousel_slider/carousel_controller.dart'; import 'package:flutter/material.dart'; import 'package:smooth_app/helpers/haptic_feedback_helper.dart'; -class ExternalCarouselManager extends StatefulWidget { - const ExternalCarouselManager({ +/// Allow to control the [ScanPageCarousel] from outside +class ExternalScanCarouselManager extends StatefulWidget { + const ExternalScanCarouselManager({ super.key, required this.child, }); final Widget child; - static ExternalCarouselManagerState watch(BuildContext context) { + static ExternalScanCarouselManagerState watch(BuildContext context) { return context .dependOnInheritedWidgetOfExactType<_InheritedCarouselManager>()! .state; } - static ExternalCarouselManagerState? find(BuildContext context) { + static ExternalScanCarouselManagerState? find(BuildContext context) { return context .findAncestorWidgetOfExactType<_InheritedCarouselManager>() ?.state; } - static ExternalCarouselManagerState read(BuildContext context) { + static ExternalScanCarouselManagerState read(BuildContext context) { return find(context)!; } @override - State createState() => - ExternalCarouselManagerState(); + State createState() => + ExternalScanCarouselManagerState(); } -class ExternalCarouselManagerState extends State { +class ExternalScanCarouselManagerState + extends State { final CarouselController _controller = CarouselController(); /// A hidden attribute to force to return to the Scanner tab @@ -75,7 +77,7 @@ class ExternalCarouselManagerState extends State { CarouselController get controller => _controller; - bool updateShouldNotify(ExternalCarouselManagerState oldState) { + bool updateShouldNotify(ExternalScanCarouselManagerState oldState) { return oldState.currentBarcode != currentBarcode || _forceShowScannerTab; } } @@ -87,7 +89,7 @@ class _InheritedCarouselManager extends InheritedWidget { Key? key, }) : super(key: key, child: child); - final ExternalCarouselManagerState state; + final ExternalScanCarouselManagerState state; @override bool updateShouldNotify(_InheritedCarouselManager oldWidget) { diff --git a/packages/smooth_app/lib/pages/scan/scan_page.dart b/packages/smooth_app/lib/pages/scan/scan_page.dart index 500972deb9c..f1a4f4401ea 100644 --- a/packages/smooth_app/lib/pages/scan/scan_page.dart +++ b/packages/smooth_app/lib/pages/scan/scan_page.dart @@ -15,7 +15,7 @@ import 'package:smooth_app/helpers/camera_helper.dart'; import 'package:smooth_app/helpers/haptic_feedback_helper.dart'; import 'package:smooth_app/helpers/permission_helper.dart'; import 'package:smooth_app/pages/scan/camera_scan_page.dart'; -import 'package:smooth_app/widgets/smooth_product_carousel.dart'; +import 'package:smooth_app/pages/scan/carousel/scan_carousel.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; class ScanPage extends StatefulWidget { @@ -92,8 +92,7 @@ class _ScanPageState extends State { flex: _carouselHeightPct, child: Padding( padding: const EdgeInsetsDirectional.only(bottom: 10.0), - child: SmoothProductCarousel( - containSearchCard: true, + child: ScanPageCarousel( onPageChangedTo: (int page, String? barcode) async { if (barcode == null) { // We only notify for new products diff --git a/packages/smooth_app/lib/widgets/smooth_product_carousel.dart b/packages/smooth_app/lib/widgets/smooth_product_carousel.dart deleted file mode 100644 index d5d806235c0..00000000000 --- a/packages/smooth_app/lib/widgets/smooth_product_carousel.dart +++ /dev/null @@ -1,375 +0,0 @@ -import 'package:carousel_slider/carousel_slider.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:provider/provider.dart'; -import 'package:scanner_shared/scanner_shared.dart' hide EMPTY_WIDGET; -import 'package:smooth_app/cards/product_cards/smooth_product_card_error.dart'; -import 'package:smooth_app/cards/product_cards/smooth_product_card_loading.dart'; -import 'package:smooth_app/cards/product_cards/smooth_product_card_not_found.dart'; -import 'package:smooth_app/cards/product_cards/smooth_product_card_thanks.dart'; -import 'package:smooth_app/data_models/continuous_scan_model.dart'; -import 'package:smooth_app/data_models/tagline/tagline_provider.dart'; -import 'package:smooth_app/generic_lib/design_constants.dart'; -import 'package:smooth_app/generic_lib/widgets/smooth_card.dart'; -import 'package:smooth_app/helpers/provider_helper.dart'; -import 'package:smooth_app/helpers/strings_helper.dart'; -import 'package:smooth_app/pages/carousel_manager.dart'; -import 'package:smooth_app/pages/navigator/app_navigator.dart'; -import 'package:smooth_app/pages/scan/scan_product_card_loader.dart'; -import 'package:smooth_app/pages/scan/scan_tagline.dart'; -import 'package:smooth_app/resources/app_icons.dart'; -import 'package:smooth_app/themes/smooth_theme_colors.dart'; -import 'package:smooth_app/themes/theme_provider.dart'; - -class SmoothProductCarousel extends StatefulWidget { - const SmoothProductCarousel({ - this.containSearchCard = false, - this.onPageChangedTo, - }); - - final bool containSearchCard; - final Function(int page, String? productBarcode)? onPageChangedTo; - - @override - State createState() => _SmoothProductCarouselState(); -} - -class _SmoothProductCarouselState extends State { - static const double HORIZONTAL_SPACE_BETWEEN_CARDS = 5.0; - - List barcodes = []; - String? _lastConsultedBarcode; - int? _carrouselMovingTo; - int _lastIndex = 0; - - int get _searchCardAdjustment => widget.containSearchCard ? 1 : 0; - late ContinuousScanModel _model; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _model = context.watch(); - - if (!ExternalCarouselManager.read(context).controller.ready) { - return; - } - - barcodes = _model.getBarcodes(); - - if (barcodes.isEmpty) { - // Ensure to reset all variables - _lastConsultedBarcode = null; - _carrouselMovingTo = null; - _lastIndex = 0; - return; - } else if (_lastConsultedBarcode == _model.latestConsultedBarcode) { - // Prevent multiple irrelevant movements - return; - } - - _lastConsultedBarcode = _model.latestConsultedBarcode; - final int cardsCount = barcodes.length + _searchCardAdjustment; - - if (_model.latestConsultedBarcode != null && - _model.latestConsultedBarcode!.isNotEmpty) { - final int indexBarcode = barcodes.indexOf(_model.latestConsultedBarcode!); - if (indexBarcode >= 0) { - final int indexCarousel = indexBarcode + _searchCardAdjustment; - _moveControllerTo(indexCarousel); - } else { - if (_lastIndex > cardsCount) { - _moveControllerTo(cardsCount); - } else { - _moveControllerTo(_lastIndex); - } - } - } else { - _moveControllerTo(0); - } - } - - Future _moveControllerTo(int page) async { - if (_carrouselMovingTo == null && _lastIndex != page) { - widget.onPageChangedTo?.call( - page, - page >= _searchCardAdjustment - ? barcodes[page - _searchCardAdjustment] - : null, - ); - - _carrouselMovingTo = page; - ExternalCarouselManager.read(context).animatePageTo(page); - _carrouselMovingTo = null; - } - } - - @override - Widget build(BuildContext context) { - barcodes = _model.getBarcodes(); - - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return CarouselSlider.builder( - itemCount: barcodes.length + _searchCardAdjustment, - itemBuilder: - (BuildContext context, int itemIndex, int itemRealIndex) { - return SizedBox.expand( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: HORIZONTAL_SPACE_BETWEEN_CARDS, - ), - child: widget.containSearchCard && itemIndex == 0 - ? const _MainCard() - : _getWidget(itemIndex - _searchCardAdjustment), - ), - ); - }, - carouselController: ExternalCarouselManager.watch(context).controller, - options: CarouselOptions( - enlargeCenterPage: false, - viewportFraction: _computeViewPortFraction(), - height: constraints.maxHeight, - enableInfiniteScroll: false, - onPageChanged: (int index, CarouselPageChangedReason reason) { - _lastIndex = index; - - if (index > 0) { - if (reason == CarouselPageChangedReason.manual) { - _model.lastConsultedBarcode = - barcodes[index - _searchCardAdjustment]; - _lastConsultedBarcode = _model.latestConsultedBarcode; - } - } else if (index == 0) { - _model.lastConsultedBarcode = null; - _lastConsultedBarcode = null; - } - }, - ), - ); - }, - ); - } - - /// Displays the card for this [index] of a list of [barcodes] - /// - /// There are special cases when the item display is refreshed - /// after the product disappeared and before the whole carousel is refreshed. - /// In those cases, we don't want the app to crash and display a Container - /// instead in the meanwhile. - Widget _getWidget(final int index) { - if (index >= barcodes.length) { - return EMPTY_WIDGET; - } - final String barcode = barcodes[index]; - switch (_model.getBarcodeState(barcode)!) { - case ScannedProductState.FOUND: - case ScannedProductState.CACHED: - return ScanProductCardLoader(barcode); - case ScannedProductState.LOADING: - return SmoothProductCardLoading( - barcode: barcode, - onRemoveProduct: (_) => _model.removeBarcode(barcode), - ); - case ScannedProductState.NOT_FOUND: - return SmoothProductCardNotFound( - barcode: barcode, - onAddProduct: () async { - await _model.refresh(); - setState(() {}); - }, - onRemoveProduct: (_) => _model.removeBarcode(barcode), - ); - case ScannedProductState.THANKS: - return const SmoothProductCardThanks(); - case ScannedProductState.ERROR_INTERNET: - return SmoothProductCardError( - barcode: barcode, - errorType: ScannedProductState.ERROR_INTERNET, - ); - case ScannedProductState.ERROR_INVALID_CODE: - return SmoothProductCardError( - barcode: barcode, - errorType: ScannedProductState.ERROR_INVALID_CODE, - ); - } - } - - double _computeViewPortFraction() { - final double screenWidth = MediaQuery.sizeOf(context).width; - if (barcodes.isEmpty) { - return 0.95; - } - - return (screenWidth - - (SmoothBarcodeScannerVisor.CORNER_PADDING * 2) - - (SmoothBarcodeScannerVisor.STROKE_WIDTH * 2) + - (HORIZONTAL_SPACE_BETWEEN_CARDS * 4)) / - screenWidth; - } -} - -class _MainCard extends StatelessWidget { - const _MainCard(); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Expanded( - child: ConsumerFilter( - buildWhen: - (TagLineProvider? previousValue, TagLineProvider currentValue) { - return previousValue?.hasContent != currentValue.hasContent; - }, - builder: (BuildContext context, TagLineProvider tagLineManager, _) { - if (!tagLineManager.hasContent) { - return const _SearchCard( - expandedMode: true, - ); - } else { - return const Column( - children: [ - Expanded( - flex: 6, - child: _SearchCard( - expandedMode: false, - ), - ), - SizedBox(height: MEDIUM_SPACE), - Expanded( - flex: 4, - child: ScanTagLine(), - ), - ], - ); - } - }, - ), - ), - ], - ); - } -} - -class _SearchCard extends StatelessWidget { - const _SearchCard({ - required this.expandedMode, - }); - - /// Expanded is when this card is the only one (no tagline, no app review…) - final bool expandedMode; - - @override - Widget build(BuildContext context) { - final AppLocalizations localizations = AppLocalizations.of(context); - final bool lightTheme = !context.watch().isDarkMode(context); - - final Widget widget = SmoothCard( - color: lightTheme ? Colors.grey.withOpacity(0.1) : Colors.black, - padding: const EdgeInsets.symmetric( - vertical: MEDIUM_SPACE, - horizontal: LARGE_SPACE, - ), - margin: const EdgeInsets.symmetric( - horizontal: 0.0, - vertical: VERY_SMALL_SPACE, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - SvgPicture.asset( - lightTheme - ? 'assets/app/logo_text_black.svg' - : 'assets/app/logo_text_white.svg', - semanticsLabel: localizations.homepage_main_card_logo_description, - ), - FormattedText( - text: localizations.homepage_main_card_subheading, - textAlign: TextAlign.center, - textStyle: const TextStyle(height: 1.3), - ), - const _SearchBar(), - ], - ), - ); - - if (expandedMode) { - return ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.sizeOf(context).height * 0.4, - ), - child: widget, - ); - } else { - return widget; - } - } -} - -class _SearchBar extends StatelessWidget { - const _SearchBar(); - - static const double SEARCH_BAR_HEIGHT = 47.0; - - @override - Widget build(BuildContext context) { - final AppLocalizations localizations = AppLocalizations.of(context); - final SmoothColorsThemeExtension theme = - Theme.of(context).extension()!; - final bool lightTheme = !context.watch().isDarkMode(context); - - return SizedBox( - height: SEARCH_BAR_HEIGHT, - child: InkWell( - onTap: () => AppNavigator.of(context).push(AppRoutes.SEARCH), - borderRadius: BorderRadius.circular(30.0), - child: Ink( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30.0), - color: lightTheme ? Colors.white : theme.greyDark, - border: Border.all(color: theme.primaryBlack), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsetsDirectional.only( - start: 20.0, - end: 10.0, - bottom: 3.0, - ), - child: Text( - localizations.homepage_main_card_search_field_hint, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: lightTheme ? Colors.black : Colors.white, - ), - ), - ), - ), - AspectRatio( - aspectRatio: 1.0, - child: DecoratedBox( - decoration: BoxDecoration( - color: theme.primaryDark, - shape: BoxShape.circle, - ), - child: const Padding( - padding: EdgeInsets.all(10.0), - child: Search( - size: 20.0, - color: Colors.white, - ), - ), - ), - ) - ], - ), - ), - ), - ); - } -}