From 23288dfc449c40b728af9e7fe77f110310f31046 Mon Sep 17 00:00:00 2001 From: Edouard Marquez Date: Mon, 13 Jan 2025 14:17:00 +0100 Subject: [PATCH] feat: Less annoying floating spam (#6190) * Less annoying floating spam * Ooops, remove that test, Edouard * No message for pictures * Better edit product page banner --- .../lib/background/background_task.dart | 2 +- .../background/background_task_details.dart | 5 +- .../lib/background/background_task_image.dart | 5 +- .../lib/background/background_task_price.dart | 2 +- .../background/background_task_unselect.dart | 2 +- packages/smooth_app/lib/l10n/app_en.arb | 21 +++- packages/smooth_app/lib/l10n/app_fr.arb | 21 +++- .../lib/pages/navigator/app_navigator.dart | 2 +- .../lib/pages/navigator/external_page.dart | 65 +++++++---- .../lib/pages/product/edit_product_page.dart | 53 +++++++-- .../smooth_app/lib/widgets/smooth_banner.dart | 108 +++++++++++------- .../lib/widgets/smooth_floating_message.dart | 108 ++++++++++++++---- 12 files changed, 288 insertions(+), 106 deletions(-) diff --git a/packages/smooth_app/lib/background/background_task.dart b/packages/smooth_app/lib/background/background_task.dart index 9fc18ecd4936..24262fdeb99d 100644 --- a/packages/smooth_app/lib/background/background_task.dart +++ b/packages/smooth_app/lib/background/background_task.dart @@ -146,7 +146,7 @@ abstract class BackgroundTask { final String message, final AlignmentGeometry alignment, )) { - SmoothFloatingMessage(message: message).show( + SmoothFloatingMessage.loading(message: message).show( context, duration: SnackBarDuration.medium, alignment: alignment, diff --git a/packages/smooth_app/lib/background/background_task_details.dart b/packages/smooth_app/lib/background/background_task_details.dart index 9fd4db64b1ac..395efcea846a 100644 --- a/packages/smooth_app/lib/background/background_task_details.dart +++ b/packages/smooth_app/lib/background/background_task_details.dart @@ -100,10 +100,7 @@ class BackgroundTaskDetails extends BackgroundTaskBarcode @override (String, AlignmentGeometry)? getFloatingMessage( final AppLocalizations appLocalizations) => - ( - appLocalizations.product_task_background_schedule, - AlignmentDirectional.center, - ); + null; /// Returns a new background task about changing a product. static BackgroundTaskDetails _getNewTask( diff --git a/packages/smooth_app/lib/background/background_task_image.dart b/packages/smooth_app/lib/background/background_task_image.dart index 7ec57d6a6887..8eaa87411dfa 100644 --- a/packages/smooth_app/lib/background/background_task_image.dart +++ b/packages/smooth_app/lib/background/background_task_image.dart @@ -104,10 +104,7 @@ class BackgroundTaskImage extends BackgroundTaskUpload { @override (String, AlignmentGeometry)? getFloatingMessage( final AppLocalizations appLocalizations) => - ( - appLocalizations.image_upload_queued, - AlignmentDirectional.center, - ); + null; /// Returns a new background task about changing a product. static BackgroundTaskImage _getNewTask( diff --git a/packages/smooth_app/lib/background/background_task_price.dart b/packages/smooth_app/lib/background/background_task_price.dart index 5420102df026..8cae8d3c5208 100644 --- a/packages/smooth_app/lib/background/background_task_price.dart +++ b/packages/smooth_app/lib/background/background_task_price.dart @@ -138,7 +138,7 @@ abstract class BackgroundTaskPrice extends BackgroundTask { final AppLocalizations appLocalizations) => ( appLocalizations.add_price_queued, - AlignmentDirectional.center, + AlignmentDirectional.bottomCenter, ); @protected diff --git a/packages/smooth_app/lib/background/background_task_unselect.dart b/packages/smooth_app/lib/background/background_task_unselect.dart index 92e3b182d9fd..718386c980c1 100644 --- a/packages/smooth_app/lib/background/background_task_unselect.dart +++ b/packages/smooth_app/lib/background/background_task_unselect.dart @@ -77,7 +77,7 @@ class BackgroundTaskUnselect extends BackgroundTaskBarcode final AppLocalizations appLocalizations) => ( appLocalizations.product_task_background_schedule, - AlignmentDirectional.topCenter, + AlignmentDirectional.bottomCenter, ); /// Returns a new background task about unselecting a product image. diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index 64ef8789681c..a0bd099f5c3d 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -1398,6 +1398,14 @@ "@edit_product_label": { "description": "Edit product button label" }, + "edit_product_pending_operations_banner_title": "Uploading your edits…", + "@edit_product_pending_operations_banner_title": { + "description": "When a product has pending edits (being sent to the server), there is a message on the edit page (here is the title of the message)." + }, + "edit_product_pending_operations_banner_message": "Your edits are being **sent in the background** (or later in case of error).\nYou can continue editing other product fields.", + "@edit_product_pending_operations_banner_message": { + "description": "When a product has pending edits (being sent to the server), there is a message on the edit page. Please keep the ** syntax to make the text bold." + }, "edit_product_label_short": "Edit", "@edit_product_label_short": { "description": "Edit product button short label (only the verb)" @@ -3551,7 +3559,14 @@ "knowledge_panel_nutriscore_banner_incorrect_score_title": "Why is this Nutri-Score different from the one on the package?", "knowledge_panel_nutriscore_banner_incorrect_score_message": "There are two possible explanations:\nThe list of ingredients and/or nutrition facts are not up-to-date.\n\nWe provide the \"New calculation\" of the Nutri-Score (or V2). Please check that you have the banner \"New calculation\" on the package.", "knowledge_panel_nutriscore_banner_incorrect_score_button1": "Check ingredients", - "knowledge_panel_nutriscore_banner_incorrect_score_button2": "Check nutrition facts" - - + "knowledge_panel_nutriscore_banner_incorrect_score_button2": "Check nutrition facts", + "url_not_supported": "Unfortunately, we can't open the URL:\n{url}", + "@url_not_supported": { + "description": "Error message when the app can't open a URL", + "placeholders": { + "url": { + "type": "String" + } + } + } } \ No newline at end of file diff --git a/packages/smooth_app/lib/l10n/app_fr.arb b/packages/smooth_app/lib/l10n/app_fr.arb index 8c9d5b12bf16..985a4668dd11 100644 --- a/packages/smooth_app/lib/l10n/app_fr.arb +++ b/packages/smooth_app/lib/l10n/app_fr.arb @@ -1398,6 +1398,14 @@ "@edit_product_label": { "description": "Edit product button label" }, + "edit_product_pending_operations_banner_title": "Envoi de vos modifications…", + "@edit_product_pending_operations_banner_title": { + "description": "When a product has pending edits (being sent to the server), there is a message on the edit page (here is the title of the message)." + }, + "edit_product_pending_operations_banner_message": "Vos modifications sont en cours d'envoi en **arrière-plan** (ou plus tard en cas d'erreur).\nVous pouvez continuer d'éditer d'autres champs du produit.", + "@edit_product_pending_operations_banner_message": { + "description": "When a product has pending edits (being sent to the server), there is a message on the edit page. Please keep the ** syntax to make the text bold." + }, "edit_product_label_short": "Modifier", "@edit_product_label_short": { "description": "Edit product button short label (only the verb)" @@ -3434,7 +3442,7 @@ "@product_page_action_bar_item_disable": { "description": "Accessibility label to disable action (= make it invisible)" }, - "product_page_pending_operations_banner_title": "Téléversement de vos modifications…", + "product_page_pending_operations_banner_title": "Envoi de vos modifications…", "@product_page_pending_operations_banner_title": { "description": "When a product has pending edits (being sent to the server), there is a message on the product page (here is the title of the message)." }, @@ -3557,5 +3565,14 @@ "knowledge_panel_nutriscore_banner_incorrect_score_title": "Pourquoi ce Nutri-Score est-il différent de celui présent sur l'emballage ?", "knowledge_panel_nutriscore_banner_incorrect_score_message": "Il y a deux explications possibles :\nLa liste des ingrédients et/ou le tableau nutritionnel ne sont pas à jour.\n\nNous affichons le \"Nouveau calcul\" du Nutri-Score (ou V2). Vérifiez que le libellé \"Nouveau calcul\" est bien sur l'emballage.", "knowledge_panel_nutriscore_banner_incorrect_score_button1": "Vérifier les ingredients", - "knowledge_panel_nutriscore_banner_incorrect_score_button2": "Vérifier le tableau nutritionnel" + "knowledge_panel_nutriscore_banner_incorrect_score_button2": "Vérifier le tableau nutritionnel", + "url_not_supported": "Malheureusement, nous ne pouvez pas ouvrir l'adresse :\n{url}", + "@url_not_supported": { + "description": "Error message when the app can't open a URL", + "placeholders": { + "url": { + "type": "String" + } + } + } } \ No newline at end of file diff --git a/packages/smooth_app/lib/pages/navigator/app_navigator.dart b/packages/smooth_app/lib/pages/navigator/app_navigator.dart index 26a7df658e88..5e792b310d7c 100644 --- a/packages/smooth_app/lib/pages/navigator/app_navigator.dart +++ b/packages/smooth_app/lib/pages/navigator/app_navigator.dart @@ -495,7 +495,7 @@ class AppRoutes { static String get SIGNUP => '/${_InternalAppRoutes.SIGNUP_PAGE}'; - // Open an external link (where path is relative to the OFF website) + // Open an external link static String EXTERNAL(String path) => '/${_InternalAppRoutes.EXTERNAL_PAGE}?path=${Uri.encodeFull(path)}'; } diff --git a/packages/smooth_app/lib/pages/navigator/external_page.dart b/packages/smooth_app/lib/pages/navigator/external_page.dart index 1cab8d78b937..ace0baa4fefd 100644 --- a/packages/smooth_app/lib/pages/navigator/external_page.dart +++ b/packages/smooth_app/lib/pages/navigator/external_page.dart @@ -2,14 +2,17 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_custom_tabs/flutter_custom_tabs.dart' as tabs; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:go_router/go_router.dart'; import 'package:http/http.dart' as http; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:path/path.dart' as path; +import 'package:smooth_app/generic_lib/duration_constants.dart'; import 'package:smooth_app/helpers/launch_url_helper.dart'; import 'package:smooth_app/pages/navigator/app_navigator.dart'; import 'package:smooth_app/query/product_query.dart'; import 'package:smooth_app/services/smooth_services.dart'; +import 'package:smooth_app/widgets/smooth_floating_message.dart'; import 'package:url_launcher/url_launcher.dart'; /// This screen is only used for deep links! @@ -37,32 +40,37 @@ class _ExternalPageState extends State { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) async { - // First let's try with https://{country}.openfoodfacts.org - final OpenFoodFactsCountry country = ProductQuery.getCountry(); + try { + String? url; - String? url; - url = path.join( - 'https://${country.offTag}.openfoodfacts.org', - widget.path, - ); + if (widget.path.startsWith('http')) { + url = widget.path; + } else { + // First let's try with https://{country}.openfoodfacts.org + final OpenFoodFactsCountry country = ProductQuery.getCountry(); - if (await _testUrl(url)) { - url = null; - } + url = path.join( + 'https://${country.offTag}.openfoodfacts.org', + widget.path, + ); - // If that's not OK, let's try with world.openfoodfacts.org?lc={language} - if (url == null) { - final OpenFoodFactsLanguage language = ProductQuery.getLanguage(); + if (await _testUrl(url)) { + url = null; + } - url = path.join( - 'https://world.openfoodfacts.org', - widget.path, - ); + // If that's not OK, let's try with world.openfoodfacts.org?lc={language} + if (url == null) { + final OpenFoodFactsLanguage language = ProductQuery.getLanguage(); - url = '$url?lc=${language.offTag}'; - } + url = path.join( + 'https://world.openfoodfacts.org', + widget.path, + ); + + url = '$url?lc=${language.offTag}'; + } + } - try { if (Platform.isAndroid) { /// Custom tabs WidgetsFlutterBinding.ensureInitialized(); @@ -81,6 +89,17 @@ class _ExternalPageState extends State { } } catch (e) { Logs.e('Unable to open an external link', ex: e); + if (mounted) { + SmoothFloatingMessage( + message: + AppLocalizations.of(context).url_not_supported(widget.path), + type: SmoothFloatingMessageType.error, + ).show( + context, + duration: SnackBarDuration.long, + alignment: Alignment.bottomCenter, + ); + } } finally { if (mounted) { final bool success = AppNavigator.of(context).pop(); @@ -96,7 +115,11 @@ class _ExternalPageState extends State { @override Widget build(BuildContext context) { - return const Scaffold(); + return const Scaffold( + body: Center( + child: CircularProgressIndicator.adaptive(), + ), + ); } /// Check if an URL exist diff --git a/packages/smooth_app/lib/pages/product/edit_product_page.dart b/packages/smooth_app/lib/pages/product/edit_product_page.dart index a04a59975005..082c19de7edd 100644 --- a/packages/smooth_app/lib/pages/product/edit_product_page.dart +++ b/packages/smooth_app/lib/pages/product/edit_product_page.dart @@ -11,6 +11,7 @@ import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_back_button.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_list_tile_card.dart'; import 'package:smooth_app/helpers/analytics_helper.dart'; +import 'package:smooth_app/helpers/color_extension.dart'; import 'package:smooth_app/helpers/product_cards_helper.dart'; import 'package:smooth_app/pages/navigator/app_navigator.dart'; import 'package:smooth_app/pages/onboarding/currency_selector_helper.dart'; @@ -22,15 +23,17 @@ import 'package:smooth_app/pages/product/edit_product_barcode.dart'; import 'package:smooth_app/pages/product/gallery_view/product_image_gallery_view.dart'; import 'package:smooth_app/pages/product/nutrition_page_loaded.dart'; import 'package:smooth_app/pages/product/product_field_editor.dart'; -import 'package:smooth_app/pages/product/product_page/new_product_page_loading_indicator.dart'; import 'package:smooth_app/pages/product/product_type_extensions.dart'; import 'package:smooth_app/pages/product/simple_input_page.dart'; import 'package:smooth_app/pages/product/simple_input_page_helpers.dart'; import 'package:smooth_app/query/product_query.dart'; +import 'package:smooth_app/resources/app_animations.dart'; import 'package:smooth_app/resources/app_icons.dart' as icons; +import 'package:smooth_app/themes/smooth_theme.dart'; import 'package:smooth_app/themes/smooth_theme_colors.dart'; import 'package:smooth_app/themes/theme_provider.dart'; import 'package:smooth_app/widgets/smooth_app_bar.dart'; +import 'package:smooth_app/widgets/smooth_banner.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; /// Page where we can indirectly edit all data about a product. @@ -69,6 +72,9 @@ class _EditProductPageState extends State with UpToDateMixin { final String productBrand = getProductBrands(upToDateProduct, appLocalizations); + final bool hasUploadIndicator = UpToDateChanges(localDatabase) + .hasNotTerminatedOperations(upToDateProduct.barcode!); + return SmoothScaffold( backgroundColor: lightTheme ? theme.extension()!.primaryLight @@ -147,7 +153,10 @@ class _EditProductPageState extends State with UpToDateMixin { top: SMALL_SPACE, start: MEDIUM_SPACE, end: MEDIUM_SPACE, - bottom: MEDIUM_SPACE + MediaQuery.viewPaddingOf(context).bottom, + bottom: MEDIUM_SPACE + + (!hasUploadIndicator + ? MediaQuery.viewPaddingOf(context).bottom + : 0.0), ), controller: _controller, children: [ @@ -308,12 +317,8 @@ class _EditProductPageState extends State with UpToDateMixin { ), ), ), - bottomNavigationBar: UpToDateChanges(localDatabase) - .hasNotTerminatedOperations(upToDateProduct.barcode!) - ? const ProductPageLoadingIndicator( - addSafeArea: true, - ) - : null, + bottomNavigationBar: + hasUploadIndicator ? const _EditPageLoadingIndicator() : null, ); } @@ -408,3 +413,35 @@ class _ListTitleItem extends SmoothListTileCard { ), ); } + +class _EditPageLoadingIndicator extends StatelessWidget { + const _EditPageLoadingIndicator(); + + @override + Widget build(BuildContext context) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + final SmoothColorsThemeExtension extension = + context.extension(); + + final bool lightTheme = context.lightTheme(); + + return SmoothBanner( + icon: CloudUploadAnimation( + size: MediaQuery.sizeOf(context).width * 0.10, + ), + iconAlignment: AlignmentDirectional.center, + iconBackgroundColor: lightTheme ? extension.primaryBlack : Colors.black, + title: appLocalizations.edit_product_pending_operations_banner_title, + titleColor: lightTheme ? null : Colors.white, + titleBackgroundColor: + lightTheme ? extension.primaryMedium : Colors.black26, + contentBackgroundColor: lightTheme + ? extension.primaryMedium.lighten() + : extension.primaryUltraBlack, + contentColor: lightTheme ? null : Colors.grey[200], + topShadow: true, + content: appLocalizations.edit_product_pending_operations_banner_message, + addSafeArea: true, + ); + } +} diff --git a/packages/smooth_app/lib/widgets/smooth_banner.dart b/packages/smooth_app/lib/widgets/smooth_banner.dart index f2414a5bc120..80b447447a15 100644 --- a/packages/smooth_app/lib/widgets/smooth_banner.dart +++ b/packages/smooth_app/lib/widgets/smooth_banner.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; +import 'package:smooth_app/themes/theme_provider.dart'; import 'package:smooth_app/widgets/smooth_close_button.dart'; import 'package:smooth_app/widgets/smooth_text.dart'; @@ -14,8 +15,10 @@ class SmoothBanner extends StatelessWidget { this.iconAlignment, this.iconColor, this.iconBackgroundColor, + this.titleBackgroundColor, this.contentBackgroundColor, this.onDismissClicked, + this.borderRadius, this.addSafeArea = false, this.topShadow = false, super.key, @@ -31,10 +34,13 @@ class SmoothBanner extends StatelessWidget { final bool topShadow; final bool addSafeArea; + final BorderRadiusGeometry? borderRadius; + final Color? iconColor; final Color? iconBackgroundColor; final Color? titleColor; final Color? contentColor; + final Color? titleBackgroundColor; final Color? contentBackgroundColor; static const Color _titleColor = Color(0xFF373737); @@ -74,51 +80,65 @@ class SmoothBanner extends StatelessWidget { width: double.infinity, color: contentBackgroundColor ?? const Color(0xFFECECEC), padding: EdgeInsetsDirectional.only( - start: MEDIUM_SPACE, - end: MEDIUM_SPACE, - top: onDismissClicked != null - ? VERY_SMALL_SPACE - : BALANCED_SPACE, bottom: onDismissClicked != null ? LARGE_SPACE : MEDIUM_SPACE, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Expanded( - child: Text( - title, - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - color: titleColor ?? _titleColor, - ), - ), + ColoredBox( + color: titleBackgroundColor ?? Colors.transparent, + child: Padding( + padding: EdgeInsetsDirectional.symmetric( + horizontal: MEDIUM_SPACE, + vertical: onDismissClicked != null + ? VERY_SMALL_SPACE + : BALANCED_SPACE, ), - if (onDismissClicked != null) ...[ - const SizedBox(width: SMALL_SPACE), - SmoothCloseButton( - onClose: () => onDismissClicked!.call( - SmoothBannerDismissEvent.fromButton, + child: Row( + children: [ + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + color: titleColor ?? _titleColor, + ), + ), ), - circleColor: titleColor ?? _titleColor, - crossColor: Colors.white, - circleSize: 26.0, - crossSize: 12.0, - tooltip: AppLocalizations.of(context) - .owner_field_info_close_button, - ), - ], - ], + if (onDismissClicked != null) ...[ + const SizedBox(width: SMALL_SPACE), + SmoothCloseButton( + onClose: () => onDismissClicked!.call( + SmoothBannerDismissEvent.fromButton, + ), + circleColor: titleColor ?? _titleColor, + crossColor: Colors.white, + circleSize: 26.0, + crossSize: 12.0, + tooltip: AppLocalizations.of(context) + .owner_field_info_close_button, + ), + ], + ], + ), + ), ), if (onDismissClicked == null) - const SizedBox(height: VERY_SMALL_SPACE), - TextWithBoldParts( - text: content, - textStyle: TextStyle( - fontSize: 14.0, - color: contentColor ?? const Color(0xFF373737), + const SizedBox( + height: VERY_SMALL_SPACE, + width: MEDIUM_SPACE, + ), + Padding( + padding: const EdgeInsetsDirectional.symmetric( + horizontal: MEDIUM_SPACE), + child: TextWithBoldParts( + text: content, + textStyle: TextStyle( + fontSize: 14.0, + height: 1.6, + color: contentColor ?? const Color(0xFF373737), + ), ), ), if (bottomPadding > 0) SizedBox(height: bottomPadding), @@ -130,14 +150,24 @@ class SmoothBanner extends StatelessWidget { ), ); + if (borderRadius != null) { + child = ClipRRect( + borderRadius: borderRadius!, + child: child, + ); + } + if (topShadow) { child = DecoratedBox( - decoration: const BoxDecoration( + decoration: BoxDecoration( + borderRadius: borderRadius, boxShadow: [ BoxShadow( - color: Colors.black12, + color: context.lightTheme() + ? Colors.black12 + : const Color(0x10FFFFFF), blurRadius: 6.0, - offset: Offset(0.0, -4.0), + offset: const Offset(0.0, -4.0), ), ], ), diff --git a/packages/smooth_app/lib/widgets/smooth_floating_message.dart b/packages/smooth_app/lib/widgets/smooth_floating_message.dart index 22f0c5d31932..1646f53253f8 100644 --- a/packages/smooth_app/lib/widgets/smooth_floating_message.dart +++ b/packages/smooth_app/lib/widgets/smooth_floating_message.dart @@ -3,13 +3,30 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/duration_constants.dart'; +import 'package:smooth_app/helpers/haptic_feedback_helper.dart'; +import 'package:smooth_app/helpers/ui_helpers.dart'; +import 'package:smooth_app/resources/app_animations.dart'; +import 'package:smooth_app/themes/smooth_theme.dart'; +import 'package:smooth_app/themes/smooth_theme_colors.dart'; class SmoothFloatingMessage { SmoothFloatingMessage({ required this.message, + this.header, + this.type = SmoothFloatingMessageType.success, }); + SmoothFloatingMessage.loading({ + required this.message, + this.type = SmoothFloatingMessageType.success, + }) : header = const Padding( + padding: EdgeInsetsDirectional.only(top: SMALL_SPACE), + child: CloudUploadAnimation(size: 50.0), + ); + final String message; + final Widget? header; + final SmoothFloatingMessageType type; OverlayEntry? _entry; Timer? _autoDismissMessage; @@ -30,6 +47,9 @@ class SmoothFloatingMessage { _entry = OverlayEntry(builder: (BuildContext context) { return _SmoothFloatingMessageView( message: message, + header: header, + type: type, + onTap: hide, alignment: alignment, margin: EdgeInsetsDirectional.only( top: appBarHeight, @@ -55,13 +75,19 @@ class SmoothFloatingMessage { class _SmoothFloatingMessageView extends StatefulWidget { const _SmoothFloatingMessageView({ required this.message, + required this.type, + required this.onTap, + this.header, this.alignment, this.margin, }); final String message; + final SmoothFloatingMessageType type; + final Widget? header; final AlignmentGeometry? alignment; final EdgeInsetsGeometry? margin; + final VoidCallback onTap; @override State<_SmoothFloatingMessageView> createState() => @@ -76,7 +102,11 @@ class _SmoothFloatingMessageViewState extends State<_SmoothFloatingMessageView> void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { + onNextFrame(() { + if (widget.type == SmoothFloatingMessageType.error) { + SmoothHapticFeedback.error(); + } + setState(() { initial = false; }); @@ -85,28 +115,52 @@ class _SmoothFloatingMessageViewState extends State<_SmoothFloatingMessageView> @override Widget build(BuildContext context) { + final SmoothColorsThemeExtension extension = + context.extension(); + final SnackBarThemeData snackBarTheme = Theme.of(context).snackBarTheme; - return AnimatedOpacity( - opacity: initial ? 0.0 : 1.0, - duration: SmoothAnimationsDuration.short, - child: SafeArea( - top: false, - child: Container( - width: initial ? 0.0 : null, - height: initial ? 0.0 : null, - margin: widget.margin, - alignment: widget.alignment ?? AlignmentDirectional.topCenter, - child: Card( - elevation: 4.0, - shadowColor: Colors.black.withValues(alpha: 0.1), - color: snackBarTheme.backgroundColor, - child: Container( - padding: const EdgeInsets.all(8.0), - child: Text( - widget.message, - textAlign: TextAlign.center, - style: snackBarTheme.contentTextStyle, + Widget child = Text( + widget.message, + textAlign: TextAlign.center, + style: (snackBarTheme.contentTextStyle ?? const TextStyle()).copyWith( + color: Colors.white, + ), + ); + + if (widget.header != null) { + child = Column( + mainAxisSize: MainAxisSize.min, + children: [ + widget.header!, + const SizedBox(height: SMALL_SPACE), + child, + ], + ); + } + + return GestureDetector( + onTap: widget.onTap, + child: AnimatedOpacity( + opacity: initial ? 0.0 : 1.0, + duration: SmoothAnimationsDuration.short, + child: SafeArea( + top: false, + child: Container( + width: initial ? 0.0 : null, + height: initial ? 0.0 : null, + margin: widget.margin, + alignment: widget.alignment ?? AlignmentDirectional.topCenter, + child: Card( + elevation: 4.0, + shadowColor: Colors.black.withValues(alpha: 0.1), + shape: const RoundedRectangleBorder( + borderRadius: ROUNDED_BORDER_RADIUS, + ), + color: _getColor(extension), + child: Container( + padding: const EdgeInsets.all(SMALL_SPACE), + child: child, ), ), ), @@ -114,4 +168,16 @@ class _SmoothFloatingMessageViewState extends State<_SmoothFloatingMessageView> ), ); } + + Color _getColor(SmoothColorsThemeExtension theme) => switch (widget.type) { + SmoothFloatingMessageType.success => theme.success, + SmoothFloatingMessageType.error => theme.error, + SmoothFloatingMessageType.warning => theme.warning, + }; +} + +enum SmoothFloatingMessageType { + success, + error, + warning, }