From db6ccda32d5ae0798821cb2f0e595f9fcb3165df Mon Sep 17 00:00:00 2001 From: Edouard Marquez Date: Fri, 27 Dec 2024 18:46:53 +0100 Subject: [PATCH] feat: New layout when a product is found (#6073) * New layout when a product is found * Customizable fields * Change types --- .../smooth_product_base_card.dart | 57 +------ .../buttons/smooth_button_with_arrow.dart | 86 ++++++++++ .../lib/helpers/haptic_feedback_helper.dart | 10 ++ packages/smooth_app/lib/l10n/app_en.arb | 19 +-- .../product/common/product_dialog_helper.dart | 158 +++++++++++------- .../lib/resources/app_animations.dart | 11 +- 6 files changed, 218 insertions(+), 123 deletions(-) create mode 100644 packages/smooth_app/lib/generic_lib/buttons/smooth_button_with_arrow.dart diff --git a/packages/smooth_app/lib/cards/product_cards/smooth_product_base_card.dart b/packages/smooth_app/lib/cards/product_cards/smooth_product_base_card.dart index dbc2f7050903..412124bfd570 100644 --- a/packages/smooth_app/lib/cards/product_cards/smooth_product_base_card.dart +++ b/packages/smooth_app/lib/cards/product_cards/smooth_product_base_card.dart @@ -2,11 +2,11 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; +import 'package:smooth_app/generic_lib/buttons/smooth_button_with_arrow.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_card.dart'; import 'package:smooth_app/pages/product/hideable_container.dart'; import 'package:smooth_app/pages/scan/carousel/scan_carousel.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'; @@ -331,58 +331,9 @@ class ScanProductBaseCardButton extends StatelessWidget { @override Widget build(BuildContext context) { - final SmoothColorsThemeExtension theme = - context.extension(); - - return Align( - alignment: AlignmentDirectional.centerEnd, - child: TextButton( - onPressed: onTap, - style: ButtonStyle( - backgroundColor: WidgetStatePropertyAll(theme.primarySemiDark), - padding: const WidgetStatePropertyAll( - EdgeInsetsDirectional.symmetric( - vertical: SMALL_SPACE, - horizontal: LARGE_SPACE, - ), - ), - shape: const WidgetStatePropertyAll( - RoundedRectangleBorder( - borderRadius: CIRCULAR_BORDER_RADIUS, - ), - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsetsDirectional.only(bottom: 3.0), - child: Text( - text, - style: const TextStyle( - color: Colors.white, - fontSize: 15.0, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(width: MEDIUM_SPACE), - Container( - width: 20.0, - height: 20.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: theme.orange, - ), - padding: const EdgeInsetsDirectional.all(VERY_SMALL_SPACE), - child: const icons.Arrow.right( - color: Colors.white, - size: 12.0, - ), - ), - ], - ), - ), + return SmoothButtonWithArrow( + text: text, + onTap: onTap, ); } } diff --git a/packages/smooth_app/lib/generic_lib/buttons/smooth_button_with_arrow.dart b/packages/smooth_app/lib/generic_lib/buttons/smooth_button_with_arrow.dart new file mode 100644 index 000000000000..464a0edf7750 --- /dev/null +++ b/packages/smooth_app/lib/generic_lib/buttons/smooth_button_with_arrow.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:smooth_app/generic_lib/design_constants.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'; + +/// A button with the following layout: +/// TEXT → +class SmoothButtonWithArrow extends StatelessWidget { + const SmoothButtonWithArrow({ + required this.text, + required this.onTap, + this.padding, + this.backgroundColor, + this.textColor, + this.arrowColor, + super.key, + }); + + final String text; + final VoidCallback? onTap; + final EdgeInsetsGeometry? padding; + final Color? backgroundColor; + final Color? textColor; + final Color? arrowColor; + + @override + Widget build(BuildContext context) { + final SmoothColorsThemeExtension theme = + context.extension(); + + return Align( + alignment: AlignmentDirectional.centerEnd, + child: TextButton( + onPressed: onTap, + style: ButtonStyle( + backgroundColor: WidgetStatePropertyAll( + backgroundColor ?? theme.primarySemiDark, + ), + padding: WidgetStatePropertyAll( + padding ?? + const EdgeInsetsDirectional.symmetric( + vertical: SMALL_SPACE, + horizontal: LARGE_SPACE, + ), + ), + shape: const WidgetStatePropertyAll( + RoundedRectangleBorder( + borderRadius: CIRCULAR_BORDER_RADIUS, + ), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsetsDirectional.only(bottom: 3.0), + child: Text( + text, + style: TextStyle( + color: textColor ?? Colors.white, + fontSize: 15.0, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: MEDIUM_SPACE), + Container( + width: 20.0, + height: 20.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: arrowColor ?? theme.orange, + ), + padding: const EdgeInsetsDirectional.all(VERY_SMALL_SPACE), + child: icons.Arrow.right( + color: textColor ?? Colors.white, + size: 12.0, + ), + ), + ], + ), + ), + ); + } +} diff --git a/packages/smooth_app/lib/helpers/haptic_feedback_helper.dart b/packages/smooth_app/lib/helpers/haptic_feedback_helper.dart index 827998b1ae36..d14473be9e18 100644 --- a/packages/smooth_app/lib/helpers/haptic_feedback_helper.dart +++ b/packages/smooth_app/lib/helpers/haptic_feedback_helper.dart @@ -49,6 +49,16 @@ class SmoothHapticFeedback { return HapticFeedback.heavyImpact(); } + static Future tadam() async { + if (!(await _areHapticFeedbackEnabled())) { + return; + } + + await HapticFeedback.heavyImpact(); + await Future.delayed(const Duration(milliseconds: 150)); + return HapticFeedback.heavyImpact(); + } + static Future _areHapticFeedbackEnabled() async { return UserPreferences.getUserPreferences() .then((UserPreferences userPreferences) { diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index 88cd78852249..2a14c275f934 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -562,10 +562,15 @@ "@add_product_information_button_label": {}, "new_product": "New Product", "@new_product": {}, - "new_product_dialog_title": "You have just found a new product!", - "@new_product_dialog_title": { - "description": "Please keep it short, like 50 characters. Title of the dialog when the user searched for an unknown barcode." + "new_product_found_title": "New product found!", + "@new_product_found_title": { + "description": "Title of a dialog that informs the user that a barcode doesn't exist in the database" }, + "new_product_found_text": "Our collaborative database contains more than **3 million products**, but this barcode doesn't exist: ", + "@new_product_found_text": { + "description": "Please keep the ** syntax to make the text bold" + }, + "new_product_found_button": "Add this product", "new_product_leave_title": "Leave this page?", "@new_product_leave_title": { "description": "Alert dialog title when a user landed on the 'add new product' page, didn't input anything and tried to leave the page." @@ -574,14 +579,6 @@ "@new_product_leave_message": { "description": "Alert dialog message when a user landed on the 'add new product' page, didn't input anything and tried to leave the page." }, - "new_product_dialog_description": "Please take photos of the packaging to add this product to our common database", - "@new_product_dialog_description": { - "description": "Please keep it short, like less than 100 characters. Explanatory text of the dialog when the user searched for an unknown barcode." - }, - "new_product_dialog_illustration_description": "An illustration with unknown Nutri-Score and Eco-Score", - "@new_product_dialog_illustration_description": { - "description": "A description for accessibility of two images side by side: a Nutri-Score and an EcoScore." - }, "front_packaging_photo_button_label": "Front packaging photo", "@front_packaging_photo_button_label": {}, "confirm_front_packaging_photo_button_label": "Confirm upload of Front packaging photo", diff --git a/packages/smooth_app/lib/pages/product/common/product_dialog_helper.dart b/packages/smooth_app/lib/pages/product/common/product_dialog_helper.dart index d30a1a9b39e6..c991cfed0784 100644 --- a/packages/smooth_app/lib/pages/product/common/product_dialog_helper.dart +++ b/packages/smooth_app/lib/pages/product/common/product_dialog_helper.dart @@ -1,17 +1,23 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:smooth_app/data_models/fetched_product.dart'; import 'package:smooth_app/database/dao_product.dart'; import 'package:smooth_app/database/local_database.dart'; +import 'package:smooth_app/generic_lib/bottom_sheets/smooth_bottom_sheet.dart'; +import 'package:smooth_app/generic_lib/buttons/smooth_button_with_arrow.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart'; import 'package:smooth_app/generic_lib/loading_dialog.dart'; -import 'package:smooth_app/generic_lib/widgets/smooth_responsive.dart'; -import 'package:smooth_app/helpers/app_helper.dart'; +import 'package:smooth_app/helpers/haptic_feedback_helper.dart'; import 'package:smooth_app/pages/navigator/app_navigator.dart'; import 'package:smooth_app/query/barcode_product_query.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'; +import 'package:smooth_app/themes/theme_provider.dart'; +import 'package:smooth_app/widgets/smooth_barcode_widget.dart'; +import 'package:smooth_app/widgets/smooth_text.dart'; /// Dialog helper for product barcode search class ProductDialogHelper { @@ -54,68 +60,108 @@ class ProductDialogHelper { title: '${AppLocalizations.of(context).looking_for}: $barcode') ?? const FetchedProduct.userCancelled(); - void _openProductNotFoundDialog() => showDialog( + void _openProductNotFoundDialog() { + showSmoothModalSheet( context: context, builder: (BuildContext context) { - final double heightMultiplier = switch (context.deviceType) { - DeviceType.small => 1, - DeviceType.smartphone => 2, - DeviceType.tablet => 2.5, - DeviceType.large => 4, - }; - final AppLocalizations appLocalizations = AppLocalizations.of(context); - return SmoothAlertDialog( - body: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, + final SmoothColorsThemeExtension theme = + context.extension(); + final bool lightTheme = context.lightTheme(); + + return SmoothModalSheet( + title: appLocalizations.new_product_found_title, + prefixIndicator: true, + bodyPadding: const EdgeInsetsDirectional.only( + start: VERY_LARGE_SPACE, + end: VERY_LARGE_SPACE, + top: LARGE_SPACE, + ), + body: Stack( children: [ - SvgPicture.asset( - 'assets/onboarding/birthday-cake.svg', - package: AppHelper.APP_PACKAGE, - excludeFromSemantics: true, - ), - SizedBox(height: SMALL_SPACE * heightMultiplier), - Text( - appLocalizations.new_product_dialog_title, - style: Theme.of(context).textTheme.displayMedium, - textAlign: TextAlign.center, - maxLines: 2, - ), - SizedBox(height: SMALL_SPACE * heightMultiplier), - Text( - appLocalizations.barcode_barcode(barcode), - textAlign: TextAlign.center, + PositionedDirectional( + bottom: 0.0, + start: 5.0, + child: Transform.scale( + scale: -1.1, + child: const OrangeErrorAnimation( + sizeMultiplier: 1.2, + ), + ), ), - SizedBox(height: SMALL_SPACE * heightMultiplier), - Text( - appLocalizations.new_product_dialog_description, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - maxLines: 3, + Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextWithBubbleParts( + text: appLocalizations.new_product_found_text, + backgroundColor: theme.primarySemiDark, + textStyle: const TextStyle( + fontSize: 15.5, + ), + bubbleTextStyle: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + fontSize: 14.5, + ), + bubblePadding: const EdgeInsetsDirectional.only( + top: 2.5, + bottom: 3.5, + start: 10.0, + end: 10.0, + ), + ), + const SizedBox(height: LARGE_SPACE), + DecoratedBox( + decoration: BoxDecoration( + borderRadius: ANGULAR_BORDER_RADIUS, + color: + lightTheme ? theme.primaryMedium : theme.primaryLight, + ), + child: SmoothBarcodeWidget( + barcode: barcode, + height: 75.0, + padding: const EdgeInsetsDirectional.only( + top: MEDIUM_SPACE, + start: VERY_LARGE_SPACE, + end: VERY_LARGE_SPACE, + bottom: MEDIUM_SPACE, + ), + color: Colors.black, + backgroundColor: + lightTheme ? Colors.white : Colors.transparent, + ), + ), + const SizedBox(height: MEDIUM_SPACE * 2), + Align( + alignment: AlignmentDirectional.centerEnd, + child: SmoothButtonWithArrow( + text: appLocalizations.new_product_found_button, + onTap: () async { + await AppNavigator.of(context).push( + AppRoutes.PRODUCT_CREATOR(barcode), + ); + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + SizedBox( + height: + MediaQuery.of(context).viewPadding.bottom + SMALL_SPACE, + ), + ], ), ], ), - actionsAxis: Axis.vertical, - positiveAction: SmoothActionButton( - text: AppLocalizations.of(context).contribute, - onPressed: () async { - await AppNavigator.of(context).push( - AppRoutes.PRODUCT_CREATOR(barcode), - ); - - if (context.mounted) { - Navigator.pop(context); - } - }, - ), - negativeAction: SmoothActionButton( - text: AppLocalizations.of(context).close, - onPressed: () => Navigator.pop(context), - ), ); - }); + }, + ); + + SmoothHapticFeedback.tadam(); + } static Widget getErrorMessage(final String message) => Row( children: [ diff --git a/packages/smooth_app/lib/resources/app_animations.dart b/packages/smooth_app/lib/resources/app_animations.dart index 866442f6791c..e997a5f4305e 100644 --- a/packages/smooth_app/lib/resources/app_animations.dart +++ b/packages/smooth_app/lib/resources/app_animations.dart @@ -230,7 +230,12 @@ class _DoubleChevronAnimationState extends State { } class OrangeErrorAnimation extends StatefulWidget { - const OrangeErrorAnimation({super.key}); + const OrangeErrorAnimation({ + this.sizeMultiplier = 1.0, + super.key, + }); + + final double sizeMultiplier; @override State createState() => _OrangeErrorAnimationState(); @@ -243,8 +248,8 @@ class _OrangeErrorAnimationState extends State { Widget build(BuildContext context) { return ExcludeSemantics( child: SizedBox( - width: 83.0, - height: 77.0, + width: 83.0 * widget.sizeMultiplier, + height: 77.0 * widget.sizeMultiplier, child: Consumer( builder: (BuildContext context, RiveFile? riveFile, _) { if (riveFile == null) {