diff --git a/packages/smooth_app/lib/cards/product_cards/smooth_product_image.dart b/packages/smooth_app/lib/cards/product_cards/smooth_product_image.dart index 4636cb32f31..01929bf735f 100644 --- a/packages/smooth_app/lib/cards/product_cards/smooth_product_image.dart +++ b/packages/smooth_app/lib/cards/product_cards/smooth_product_image.dart @@ -280,7 +280,7 @@ class _ProductPictureWithImageProvider extends StatelessWidget { color: lightTheme ? Colors.white : Colors.black, child: ClipRRect( child: Opacity( - opacity: lightTheme ? 0.2 : 0.55, + opacity: lightTheme ? 0.3 : 0.55, child: ImageFiltered( imageFilter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0), child: Image( diff --git a/packages/smooth_app/lib/helpers/color_extension.dart b/packages/smooth_app/lib/helpers/color_extension.dart new file mode 100644 index 00000000000..d67adfa98b6 --- /dev/null +++ b/packages/smooth_app/lib/helpers/color_extension.dart @@ -0,0 +1,24 @@ +import 'package:flutter/painting.dart'; + +/// Code from https://stackoverflow.com/questions/58360989 +extension ColorExtension on Color { + Color darken([double amount = .1]) { + assert(amount >= 0 && amount <= 1); + + final HSLColor hsl = HSLColor.fromColor(this); + final HSLColor hslDark = + hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0)); + + return hslDark.toColor(); + } + + Color lighten([double amount = .1]) { + assert(amount >= 0 && amount <= 1); + + final HSLColor hsl = HSLColor.fromColor(this); + final HSLColor hslLight = + hsl.withLightness((hsl.lightness + amount).clamp(0.0, 1.0)); + + return hslLight.toColor(); + } +} diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index b6951e8065a..e09c73418a6 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -3277,6 +3277,14 @@ "@product_page_action_bar_item_disable": { "description": "Accessibility label to disable action (= make it invisible)" }, + "product_page_pending_operations_banner_title": "Uploading your edits…", + "@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)." + }, + "product_page_pending_operations_banner_message": "The data displayed on this page **does not yet reflect your modifications**.\nPlease wait a few seconds…", + "@product_page_pending_operations_banner_message": { + "description": "When a product has pending edits (being sent to the server), there is a message on the product page. Please keep the ** syntax to make the text bold." + }, "product_add_a_language": "Add a language", "@product_add_a_language": { "description": "Button to add a language (eg: for photos) to a product" diff --git a/packages/smooth_app/lib/pages/input/smooth_autocomplete_text_field.dart b/packages/smooth_app/lib/pages/input/smooth_autocomplete_text_field.dart index 68bbdf505f3..8d1456341cc 100644 --- a/packages/smooth_app/lib/pages/input/smooth_autocomplete_text_field.dart +++ b/packages/smooth_app/lib/pages/input/smooth_autocomplete_text_field.dart @@ -151,7 +151,11 @@ class _SmoothAutocompleteTextFieldState void _setLoading(bool loading) { if (_loading != loading) { WidgetsBinding.instance.addPostFrameCallback( - (_) => setState(() => _loading = loading), + (_) { + if (context.mounted) { + setState(() => _loading = loading); + } + }, ); } } diff --git a/packages/smooth_app/lib/pages/product/product_page/new_product_header.dart b/packages/smooth_app/lib/pages/product/product_page/new_product_header.dart index 1d97a3c199d..25f8cd3b5ca 100644 --- a/packages/smooth_app/lib/pages/product/product_page/new_product_header.dart +++ b/packages/smooth_app/lib/pages/product/product_page/new_product_header.dart @@ -208,7 +208,10 @@ class _ProductHeaderName extends StatelessWidget { style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 17.0, - height: 0.9, + height: 1.0, + ), + strutStyle: const StrutStyle( + forceStrutHeight: true, ), ), Text( @@ -286,6 +289,9 @@ class _ProductCompatibilityScore extends StatelessWidget { BuildContext context, ProductPageCompatibility compatibility, ) { + final String compatibilityLabel = + AppLocalizations.of(context).product_page_compatibility_score; + return IntrinsicHeight( child: Row( children: [ @@ -314,15 +320,19 @@ class _ProductCompatibilityScore extends StatelessWidget { ), Expanded( child: Padding( - padding: const EdgeInsets.only( + padding: const EdgeInsetsDirectional.only( top: 6.0, - bottom: 8.0, + bottom: SMALL_SPACE, + start: 6.0, + end: 6.0, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( '${compatibility.score}%', + maxLines: 1, + textAlign: TextAlign.center, style: const TextStyle( fontSize: 12.0, height: 0.9, @@ -330,10 +340,12 @@ class _ProductCompatibilityScore extends StatelessWidget { ), ), Text( - AppLocalizations.of(context) - .product_page_compatibility_score, - style: const TextStyle( - fontSize: 9.0, + compatibilityLabel, + maxLines: 1, + textAlign: TextAlign.center, + overflow: TextOverflow.fade, + style: TextStyle( + fontSize: _getCompatibilityFontSize(compatibilityLabel), height: 0.9, fontWeight: FontWeight.w500, ), @@ -347,6 +359,24 @@ class _ProductCompatibilityScore extends StatelessWidget { ); } + double _getCompatibilityFontSize(String compatibilityLabel) { + final int length = compatibilityLabel.length; + + if (length < 13) { + return 9.0; + } else if (length == 13) { + return 8.5; + } else if (length == 14) { + return 7.5; + } else if (length == 15) { + return 7.0; + } else if (length == 16) { + return 6.5; + } else { + return 6.0; + } + } + double computeWidth(BuildContext context) { return math.min( 80.0, diff --git a/packages/smooth_app/lib/pages/product/product_page/new_product_page.dart b/packages/smooth_app/lib/pages/product/product_page/new_product_page.dart index 041698d206d..be0227ae687 100644 --- a/packages/smooth_app/lib/pages/product/product_page/new_product_page.dart +++ b/packages/smooth_app/lib/pages/product/product_page/new_product_page.dart @@ -7,6 +7,7 @@ import 'package:provider/single_child_widget.dart'; import 'package:smooth_app/data_models/preferences/user_preferences.dart'; import 'package:smooth_app/data_models/product_list.dart'; import 'package:smooth_app/data_models/product_preferences.dart'; +import 'package:smooth_app/data_models/up_to_date_changes.dart'; import 'package:smooth_app/data_models/up_to_date_mixin.dart'; import 'package:smooth_app/database/dao_product_last_access.dart'; import 'package:smooth_app/database/dao_product_list.dart'; @@ -20,6 +21,7 @@ import 'package:smooth_app/pages/prices/prices_card.dart'; import 'package:smooth_app/pages/product/common/product_refresher.dart'; import 'package:smooth_app/pages/product/product_page/footer/new_product_footer.dart'; import 'package:smooth_app/pages/product/product_page/new_product_header.dart'; +import 'package:smooth_app/pages/product/product_page/new_product_page_loading_indicator.dart'; import 'package:smooth_app/pages/product/product_questions_widget.dart'; import 'package:smooth_app/pages/product/reorderable_knowledge_panel_page.dart'; import 'package:smooth_app/pages/product/reordered_knowledge_panel_cards.dart'; @@ -83,7 +85,7 @@ class ProductPageState extends State Theme.of(context).extension()!; _productPreferences = context.watch(); - context.watch(); + final LocalDatabase localDatabase = context.watch(); refreshUpToDate(); final MatchedProductV2 matchedProductV2 = MatchedProductV2( @@ -91,6 +93,9 @@ class ProductPageState extends State _productPreferences, ); + final bool hasPendingOperations = UpToDateChanges(localDatabase) + .hasNotTerminatedOperations(upToDateProduct.barcode!); + return MultiProvider( providers: [ Provider.value(value: upToDateProduct), @@ -135,9 +140,11 @@ class ProductPageState extends State setState(() => bottomPadding = size.height); } }, - child: ProductQuestionsWidget( - upToDateProduct, - ), + child: !hasPendingOperations + ? const ProductPageLoadingIndicator() + : ProductQuestionsWidget( + upToDateProduct, + ), ), ), ], diff --git a/packages/smooth_app/lib/pages/product/product_page/new_product_page_loading_indicator.dart b/packages/smooth_app/lib/pages/product/product_page/new_product_page_loading_indicator.dart new file mode 100644 index 00000000000..62ab9ed935a --- /dev/null +++ b/packages/smooth_app/lib/pages/product/product_page/new_product_page_loading_indicator.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; +import 'package:smooth_app/helpers/color_extension.dart'; +import 'package:smooth_app/pages/product/product_page/new_product_page.dart'; +import 'package:smooth_app/resources/app_animations.dart'; +import 'package:smooth_app/themes/theme_provider.dart'; +import 'package:smooth_app/widgets/smooth_banner.dart'; + +class ProductPageLoadingIndicator extends StatelessWidget { + const ProductPageLoadingIndicator({super.key}); + + @override + Widget build(BuildContext context) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + + final bool lightTheme = context.lightTheme(); + final Color color = context.watch().color ?? + (lightTheme ? Colors.grey : Colors.grey[600]!); + + return SmoothBanner( + icon: CloudUploadAnimation( + size: MediaQuery.sizeOf(context).width * 0.10, + ), + iconAlignment: AlignmentDirectional.center, + iconBackgroundColor: color, + title: appLocalizations.product_page_pending_operations_banner_title, + titleColor: lightTheme ? null : Colors.white, + contentBackgroundColor: + lightTheme ? color.lighten(0.6) : color.darken(0.3), + contentColor: lightTheme ? null : Colors.grey[200], + topShadow: true, + content: appLocalizations.product_page_pending_operations_banner_message, + ); + } +} diff --git a/packages/smooth_app/lib/resources/app_animations.dart b/packages/smooth_app/lib/resources/app_animations.dart index 80e82d5615c..866442f6791 100644 --- a/packages/smooth_app/lib/resources/app_animations.dart +++ b/packages/smooth_app/lib/resources/app_animations.dart @@ -3,6 +3,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; import 'package:rive/rive.dart'; +// ignore: implementation_imports +import 'package:rive/src/rive_core/component.dart'; import 'package:scanner_shared/scanner_shared.dart'; import 'package:smooth_app/cards/category_cards/svg_cache.dart'; import 'package:smooth_app/helpers/haptic_feedback_helper.dart'; @@ -85,16 +87,19 @@ class BarcodeAnimation extends StatelessWidget { class CloudUploadAnimation extends StatelessWidget { const CloudUploadAnimation({ required this.size, + this.color, super.key, }) : _circleColor = null; const CloudUploadAnimation.circle({ required this.size, + this.color, Color? circleColor, super.key, }) : _circleColor = circleColor ?? Colors.black54; final double size; + final Color? color; final Color? _circleColor; @override @@ -105,6 +110,19 @@ class CloudUploadAnimation extends StatelessWidget { AnimationsLoader.of(context)!, artboard: 'Cloud upload', animations: const ['Animation'], + onInit: (Artboard artboard) { + if (color != null) { + artboard.forEachComponent( + (Component child) { + if (child is Stroke) { + child.paint.color = color!; + } else if (child is SolidColor) { + child.color = color!; + } + }, + ); + } + }, ), ); diff --git a/packages/smooth_app/lib/widgets/smooth_banner.dart b/packages/smooth_app/lib/widgets/smooth_banner.dart index 085167f3beb..3b67afece8f 100644 --- a/packages/smooth_app/lib/widgets/smooth_banner.dart +++ b/packages/smooth_app/lib/widgets/smooth_banner.dart @@ -2,17 +2,25 @@ 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/widgets/smooth_close_button.dart'; +import 'package:smooth_app/widgets/smooth_text.dart'; class SmoothBanner extends StatelessWidget { const SmoothBanner({ required this.icon, required this.title, required this.content, + this.titleColor, + this.contentColor, + this.iconAlignment, + this.iconColor, + this.iconBackgroundColor, + this.contentBackgroundColor, this.onDismissClicked, this.topShadow = false, super.key, }); + final AlignmentGeometry? iconAlignment; final Widget icon; final String title; final String content; @@ -21,6 +29,12 @@ class SmoothBanner extends StatelessWidget { final ValueChanged? onDismissClicked; final bool topShadow; + final Color? iconColor; + final Color? iconBackgroundColor; + final Color? titleColor; + final Color? contentColor; + final Color? contentBackgroundColor; + static const Color _titleColor = Color(0xFF373737); @override @@ -34,15 +48,15 @@ class SmoothBanner extends StatelessWidget { child: Container( width: double.infinity, height: double.infinity, - color: const Color(0xFFE4E4E4), + color: iconBackgroundColor ?? const Color(0xFFE4E4E4), padding: const EdgeInsetsDirectional.symmetric( horizontal: LARGE_SPACE, vertical: MEDIUM_SPACE, ), - alignment: AlignmentDirectional.topCenter, + alignment: iconAlignment ?? AlignmentDirectional.topCenter, child: IconTheme( - data: const IconThemeData( - color: Color(0xFF373737), + data: IconThemeData( + color: iconColor ?? const Color(0xFF373737), ), child: icon, ), @@ -53,7 +67,7 @@ class SmoothBanner extends StatelessWidget { flex: 85, child: Container( width: double.infinity, - color: const Color(0xFFECECEC), + color: contentBackgroundColor ?? const Color(0xFFECECEC), padding: EdgeInsetsDirectional.only( start: MEDIUM_SPACE, end: MEDIUM_SPACE, @@ -70,10 +84,10 @@ class SmoothBanner extends StatelessWidget { Expanded( child: Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, - color: _titleColor, + color: titleColor ?? _titleColor, ), ), ), @@ -83,7 +97,7 @@ class SmoothBanner extends StatelessWidget { onClose: () => onDismissClicked!.call( SmoothBannerDismissEvent.fromButton, ), - circleColor: _titleColor, + circleColor: titleColor ?? _titleColor, crossColor: Colors.white, circleSize: 26.0, crossSize: 12.0, @@ -95,11 +109,11 @@ class SmoothBanner extends StatelessWidget { ), if (onDismissClicked == null) const SizedBox(height: VERY_SMALL_SPACE), - Text( - content, - style: const TextStyle( + TextWithBoldParts( + text: content, + textStyle: TextStyle( fontSize: 14.0, - color: Color(0xFF373737), + color: contentColor ?? const Color(0xFF373737), ), ), ],