refactor: Rename tagline* to AppNews (#5377)

* Rename `tagline*` to `AppNews`

* Fix labeler path
This commit is contained in:
Edouard Marquez
2024-06-16 17:39:33 +02:00
committed by GitHub
parent 4fbf0203ac
commit a3e9bc24ec
18 changed files with 533 additions and 534 deletions

2
.github/labeler.yml vendored
View File

@ -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/pages/scan/carousel/scan_carousel.dart'
✏️ Editing - 📦 Packaging input:
- changed-files:

View File

@ -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<dynamic, dynamic> json)
@ -15,17 +15,16 @@ class _TagLineJSON {
final _TagLineJSONNewsList news;
final _TaglineJSONFeed taglineFeed;
TagLine toTagLine(String locale) {
final Map<String, TagLineNewsItem> tagLineNews = news.map(
(String key, _TagLineItemNewsItem value) =>
MapEntry<String, TagLineNewsItem>(
AppNews toTagLine(String locale) {
final Map<String, AppNewsItem> tagLineNews = news.map(
(String key, _TagLineItemNewsItem value) => MapEntry<String, AppNewsItem>(
key,
value.toTagLineItem(locale),
),
);
final _TagLineJSONFeedLocale localizedFeed = taglineFeed.loadNews(locale);
final Iterable<TagLineFeedItem> feed = localizedFeed.news
final Iterable<AppNewsFeedItem> 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,

View File

@ -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<String, TagLineNewsItem> news) : _news = news;
class AppNewsList {
const AppNewsList(Map<String, AppNewsItem> news) : _news = news;
final Map<String, TagLineNewsItem> _news;
final Map<String, AppNewsItem> _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<TagLineFeedItem> news;
final List<AppNewsFeedItem> 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}';
}
}

View File

@ -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<void> loadTagLine({bool forceUpdate = false}) async {
_emit(const TagLineLoading());
Future<void> 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<TagLine?> _parseJSONAndGetLocalizedContent(
static Future<AppNews?> _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<String?> _fetchTagLine() async {
Future<String?> _fetchJSON() async {
try {
final UriProductHelper uriProductHelper = ProductQuery.uriProductHelper;
final Map<String, String> headers = <String, String>{};
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<File> get _tagLineCacheFile => getApplicationCacheDirectory()
Future<File> get _newsCacheFile => getApplicationCacheDirectory()
.then((Directory dir) => File(join(dir.path, 'tagline.json')));
Future<File> _saveTagLineToCache(final String json) async {
final File file = await _tagLineCacheFile;
Future<File> _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;
}

View File

@ -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<KnowledgePanelPage>
Future<void> _refreshProduct(BuildContext context) async {
try {
final String? barcode =
ExternalCarouselManager.read(context).currentBarcode;
ExternalScanCarouselManager.read(context).currentBarcode;
if (barcode?.isEmpty == true) {
return;
}

View File

@ -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';
@ -237,8 +237,8 @@ class _SmoothAppState extends State<SmoothApp> {
provide<ContinuousScanModel>(_continuousScanModel),
provide<PermissionListener>(_permissionListener),
],
child: ChangeNotifierProvider<TagLineProvider>(
create: (BuildContext context) => TagLineProvider(
child: ChangeNotifierProvider<AppNewsProvider>(
create: (BuildContext context) => AppNewsProvider(
context.read<UserPreferences>(),
),
lazy: true,

View File

@ -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<UserPreferences>());
context.read<TagLineProvider>().loadTagLine();
context.read<AppNewsProvider>().loadLatestNews();
return context.read<ProductPreferences>().refresh();
}

View File

@ -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());
}
}

View File

@ -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<PageManager> {
@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;

View File

@ -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<LocalDatabase>(),
);
// Refresh the tagline
// Refresh the news feed
if (context.mounted) {
context.read<TagLineProvider>().loadTagLine();
context.read<AppNewsProvider>().loadLatestNews();
}
// TODO(monsieurtanuki): make it a background task also?
// no await

View File

@ -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<ProductListPage>
icon: const Icon(CupertinoIcons.barcode),
label: Text(appLocalizations.product_list_empty_title),
onPressed: () =>
ExternalCarouselManager.read(context).showSearchCard(),
ExternalScanCarouselManager.read(context).showSearchCard(),
)
: _selectionMode
? null

View File

@ -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<ProductPage>
@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<ProductPreferences>();

View File

@ -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: <Widget>[
Expanded(
child: ConsumerFilter<AppNewsProvider>(
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: <Widget>[
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<ThemeProvider>().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: <Widget>[
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<SmoothColorsThemeExtension>()!;
final bool lightTheme = !context.watch<ThemeProvider>().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: <Widget>[
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,
),
),
),
)
],
),
),
),
);
}
}

View File

@ -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<TagLineNewsItem> news;
final Iterable<AppNewsItem> news;
@override
State<_ScanTagLineContent> createState() => _ScanTagLineContentState();
@ -102,7 +102,7 @@ class _ScanTagLineContentState extends State<_ScanTagLineContent> {
final ThemeProvider themeProvider = context.watch<ThemeProvider>();
final SmoothColorsThemeExtension theme =
Theme.of(context).extension<SmoothColorsThemeExtension>()!;
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<TagLineProvider>(),
class _ScanNewsFeedProvider extends ValueNotifier<_ScanTagLineState> {
_ScanNewsFeedProvider(BuildContext context)
: _newsFeedProvider = context.read<AppNewsProvider>(),
_userPreferences = context.read<UserPreferences>(),
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<void> _onTagLineContentAvailable(TagLine tagLine) async {
Future<void> _onTagLineContentAvailable(AppNews tagLine) async {
if (!tagLine.feed.isNotEmpty) {
emit(const _ScanTagLineStateNoContent());
return;
}
final List<TagLineNewsItem> unreadNews = <TagLineNewsItem>[];
final List<TagLineNewsItem> displayedNews = <TagLineNewsItem>[];
final List<TagLineNewsItem> clickedNews = <TagLineNewsItem>[];
final List<AppNewsItem> unreadNews = <AppNewsItem>[];
final List<AppNewsItem> displayedNews = <AppNewsItem>[];
final List<AppNewsItem> clickedNews = <AppNewsItem>[];
final List<String> taglineFeedAlreadyClickedNews =
_userPreferences.taglineFeedClickedNews;
final List<String> 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(
<TagLineNewsItem>[
<AppNewsItem>[
...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<TagLineNewsItem> tagLine;
final Iterable<AppNewsItem> tagLine;
}

View File

@ -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<ScanPageCarousel> createState() => _ScanPageCarouselState();
}
class _ScanPageCarouselState extends State<ScanPageCarousel> {
static const double HORIZONTAL_SPACE_BETWEEN_CARDS = 5.0;
List<String> barcodes = <String>[];
String? _lastConsultedBarcode;
int? _carrouselMovingTo;
int _lastIndex = 0;
late ContinuousScanModel _model;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_model = context.watch<ContinuousScanModel>();
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<void> _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;
}
}

View File

@ -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<ExternalCarouselManager> createState() =>
ExternalCarouselManagerState();
State<ExternalScanCarouselManager> createState() =>
ExternalScanCarouselManagerState();
}
class ExternalCarouselManagerState extends State<ExternalCarouselManager> {
class ExternalScanCarouselManagerState
extends State<ExternalScanCarouselManager> {
final CarouselController _controller = CarouselController();
/// A hidden attribute to force to return to the Scanner tab
@ -75,7 +77,7 @@ class ExternalCarouselManagerState extends State<ExternalCarouselManager> {
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) {

View File

@ -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<ScanPage> {
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

View File

@ -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<SmoothProductCarousel> createState() => _SmoothProductCarouselState();
}
class _SmoothProductCarouselState extends State<SmoothProductCarousel> {
static const double HORIZONTAL_SPACE_BETWEEN_CARDS = 5.0;
List<String> barcodes = <String>[];
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<ContinuousScanModel>();
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<void> _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: <Widget>[
Expanded(
child: ConsumerFilter<TagLineProvider>(
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: <Widget>[
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<ThemeProvider>().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: <Widget>[
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<SmoothColorsThemeExtension>()!;
final bool lightTheme = !context.watch<ThemeProvider>().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: <Widget>[
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,
),
),
),
)
],
),
),
),
);
}
}