From 41e919129a57022da0b356c773755948adaf7e42 Mon Sep 17 00:00:00 2001 From: Edouard Marquez Date: Sat, 22 Jul 2023 17:30:46 +0200 Subject: [PATCH] feat: Product loading card (#4318) --- packages/smooth_app/assets/animations/off.riv | Bin 0 -> 29011 bytes .../smooth_product_base_card.dart | 29 ++- .../smooth_product_card_loading.dart | 237 ++++++++++++++++-- .../lib/helpers/analytics_helper.dart | 9 + packages/smooth_app/lib/l10n/app_en.arb | 22 +- packages/smooth_app/lib/l10n/app_fr.arb | 20 ++ .../lib/widgets/smooth_product_carousel.dart | 5 +- .../Flutter/GeneratedPluginRegistrant.swift | 2 + packages/smooth_app/macos/Podfile.lock | 6 + 9 files changed, 289 insertions(+), 41 deletions(-) create mode 100644 packages/smooth_app/assets/animations/off.riv diff --git a/packages/smooth_app/assets/animations/off.riv b/packages/smooth_app/assets/animations/off.riv new file mode 100644 index 0000000000000000000000000000000000000000..56224533e7f71dac060de1f8b163142c9193809e GIT binary patch literal 29011 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 bQU>-eJO8(wz@K8miXN5%-X`+u-zfSYcb#0C literal 0 HcmV?d00001 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 94aeaff7797..d80f0a34fe2 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 @@ -68,19 +68,22 @@ class ProductCardCloseButton extends StatelessWidget { Widget build(BuildContext context) { final AppLocalizations appLocalizations = AppLocalizations.of(context); - return InkWell( - customBorder: const CircleBorder(), - onTap: () { - onRemove?.call(context); - SmoothHapticFeedback.lightNotification(); - }, - child: Tooltip( - message: appLocalizations.product_card_remove_product_tooltip, - child: Padding( - padding: const EdgeInsets.all(SMALL_SPACE), - child: Icon( - iconData, - size: DEFAULT_ICON_SIZE, + return Material( + type: MaterialType.transparency, + child: InkWell( + customBorder: const CircleBorder(), + onTap: () { + onRemove?.call(context); + SmoothHapticFeedback.lightNotification(); + }, + child: Tooltip( + message: appLocalizations.product_card_remove_product_tooltip, + child: Padding( + padding: const EdgeInsets.all(SMALL_SPACE), + child: Icon( + iconData, + size: DEFAULT_ICON_SIZE, + ), ), ), ), diff --git a/packages/smooth_app/lib/cards/product_cards/smooth_product_card_loading.dart b/packages/smooth_app/lib/cards/product_cards/smooth_product_card_loading.dart index b9aa282b164..87d6b4fd580 100644 --- a/packages/smooth_app/lib/cards/product_cards/smooth_product_card_loading.dart +++ b/packages/smooth_app/lib/cards/product_cards/smooth_product_card_loading.dart @@ -1,39 +1,224 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; +import 'package:rive/rive.dart'; +import 'package:smooth_app/cards/product_cards/smooth_product_base_card.dart'; +import 'package:smooth_app/data_models/continuous_scan_model.dart'; +import 'package:smooth_app/generic_lib/buttons/smooth_simple_button.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; +import 'package:smooth_app/generic_lib/duration_constants.dart'; +import 'package:smooth_app/helpers/analytics_helper.dart'; -class SmoothProductCardLoading extends StatelessWidget { - const SmoothProductCardLoading({required this.barcode}); +class SmoothProductCardLoading extends StatefulWidget { + const SmoothProductCardLoading({ + required this.barcode, + this.onRemoveProduct, + }); final String barcode; + final OnRemoveCallback? onRemoveProduct; + + @override + State createState() => + _SmoothProductCardLoadingState(); +} + +class _SmoothProductCardLoadingState extends State { + late Timer _timer; + _SmoothProductCardLoadingProgress _progress = + _SmoothProductCardLoadingProgress.initial; + + @override + void initState() { + super.initState(); + _timer = Timer(const Duration(seconds: 7), _onLongRequest); + } @override Widget build(BuildContext context) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); final ThemeData themeData = Theme.of(context); - return Container( - decoration: BoxDecoration( - color: themeData.brightness == Brightness.light - ? Colors.white - : Colors.black, - borderRadius: ROUNDED_BORDER_RADIUS, - ), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(barcode, style: Theme.of(context).textTheme.titleMedium), - ], - ), - const SizedBox( - height: 12.0, - ), - const CircularProgressIndicator.adaptive() - ], - ), + return DefaultTextStyle.merge( + textAlign: TextAlign.center, + style: const TextStyle(height: 1.4), + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Stack( + children: [ + Positioned.fill( + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + vertical: SMALL_SPACE, + horizontal: MEDIUM_SPACE, + ), + decoration: BoxDecoration( + color: themeData.brightness == Brightness.light + ? Colors.white + : Colors.black, + borderRadius: ROUNDED_BORDER_RADIUS, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const Spacer(), + Text( + appLocalizations.scan_product_loading, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18.0, + ), + ), + const Spacer(flex: 2), + Container( + padding: const EdgeInsets.symmetric( + horizontal: SMALL_SPACE, + vertical: SMALL_SPACE, + ), + color: Colors.grey.withOpacity(0.2), + child: Text( + '<${widget.barcode}>', + style: const TextStyle( + letterSpacing: 6.0, + fontFeatures: [ + FontFeature.tabularFigures(), + ], + ), + ), + ), + const Spacer(flex: 2), + AnimatedSwitcher( + duration: SmoothAnimationsDuration.long, + child: Text(_description(appLocalizations)), + transitionBuilder: + (Widget child, Animation animation) { + return FadeTransition( + opacity: Tween( + begin: 0.0, + end: 1.0, + ).animate(animation), + child: child, + ); + }, + ), + const Spacer(), + Expanded( + flex: 10, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 300, + ), + child: _progress == + _SmoothProductCardLoadingProgress.unresponsive + ? Center( + child: SmoothSimpleButton( + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.restart_alt), + const SizedBox( + width: SMALL_SPACE, + ), + Text(appLocalizations + .scan_product_loading_restart_button) + ], + ), + onPressed: () { + AnalyticsHelper.trackEvent( + AnalyticsEvent.restartProductLoading, + barcode: widget.barcode, + ); + + final ContinuousScanModel model = + context.read(); + + model.retryBarcodeFetch(widget.barcode); + }, + ), + ) + : const RiveAnimation.asset( + 'assets/animations/off.riv', + artboard: 'Loading', + alignment: Alignment.topCenter, + fit: BoxFit.fitHeight, + ), + ), + ), + const Spacer(), + ], + ), + ), + ), + if (_progress != _SmoothProductCardLoadingProgress.initial) + Positioned.directional( + top: 0.0, + end: 0.0, + textDirection: Directionality.of(context), + child: Padding( + padding: EdgeInsetsDirectional.only( + top: constraints.maxHeight * 0.05, + end: constraints.maxWidth * 0.05, + ), + child: ProductCardCloseButton( + onRemove: (BuildContext context) { + AnalyticsHelper.trackEvent( + AnalyticsEvent.ignoreProductLoading, + barcode: widget.barcode, + ); + + widget.onRemoveProduct?.call(context); + }, + iconData: CupertinoIcons.clear_circled, + ), + ), + ), + ], + ); + }), ); } + + String _description(AppLocalizations appLocalizations) { + return switch (_progress) { + _SmoothProductCardLoadingProgress.longRequest => + appLocalizations.scan_product_loading_long_request, + _SmoothProductCardLoadingProgress.unresponsive => + appLocalizations.scan_product_loading_unresponsive, + _ => appLocalizations.scan_product_loading_initial, + }; + } + + void _onLongRequest() { + if (!mounted) { + return; + } + setState(() => _progress = _SmoothProductCardLoadingProgress.longRequest); + _timer = Timer(const Duration(seconds: 5), _onUnresponsiveRequest); + } + + void _onUnresponsiveRequest() { + if (!mounted) { + return; + } + setState(() => _progress = _SmoothProductCardLoadingProgress.unresponsive); + } + + @override + void dispose() { + _timer.cancel(); + super.dispose(); + } +} + +enum _SmoothProductCardLoadingProgress { + initial, + longRequest, + unresponsive, } diff --git a/packages/smooth_app/lib/helpers/analytics_helper.dart b/packages/smooth_app/lib/helpers/analytics_helper.dart index 8d300225152..20f45fb04ee 100644 --- a/packages/smooth_app/lib/helpers/analytics_helper.dart +++ b/packages/smooth_app/lib/helpers/analytics_helper.dart @@ -13,6 +13,7 @@ enum AnalyticsCategory { userManagement(tag: 'user management'), scanning(tag: 'scanning'), share(tag: 'share'), + loadingProduct(tag: 'loading product'), couldNotFindProduct(tag: 'could not find product'), productEdit(tag: 'product edit'), productFastTrackEdit(tag: 'product fast track edit'), @@ -40,6 +41,14 @@ enum AnalyticsEvent { tag: 'could not find product', category: AnalyticsCategory.couldNotFindProduct, ), + ignoreProductLoading( + tag: 'ignore product', + category: AnalyticsCategory.loadingProduct, + ), + restartProductLoading( + tag: 'restart request', + category: AnalyticsCategory.loadingProduct, + ), ignoreProductNotFound( tag: 'ignore product', category: AnalyticsCategory.couldNotFindProduct, diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index d557b811703..0dca6a48f51 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -1955,7 +1955,27 @@ "@scan_header_compare_button_valid_state_tooltip": { "description": "Tooltip (message visible with a long-press) on the Compare button on top of the scanner, when there is at least two prodiucts" }, - "portion_calculator_description": "Calculate nutrition facts for a specific quantity:", + "scan_product_loading": "You have scanned\nthe barcode:", + "@scan_product_loading": { + "description": "Title when a product is loading (carousel card). Please ensure to keep the line break." + }, + "scan_product_loading_initial": "We're looking for this product!\nPlease wait a few seconds…", + "@scan_product_loading_initial": { + "description": "Message when a product is loading (carousel card). Please ensure to keep the line break." + }, + "scan_product_loading_long_request": "We're still looking for this product!\nDo you find it takes a long time to load? So are we…", + "@scan_product_loading_long_request": { + "description": "Message when a product is long to load (carousel card). Please ensure to keep the line break." + }, + "scan_product_loading_unresponsive": "We're still looking for this product.\nWould you like to restart the search?", + "@scan_product_loading_unresponsive": { + "description": "Message when a product is too long to load (carousel card). Please ensure to keep the line break." + }, + "scan_product_loading_restart_button": "Restart search", + "@scan_product_loading_restart_button": { + "description": "Button to force restart a product search" + }, + "portion_calculator_description": "Calculate nutrition facts for a specific quantity", "@portion_calculator_description": { "description": "Sort of title that describes the portion calculator." }, diff --git a/packages/smooth_app/lib/l10n/app_fr.arb b/packages/smooth_app/lib/l10n/app_fr.arb index e26d5de4184..eb27e076726 100644 --- a/packages/smooth_app/lib/l10n/app_fr.arb +++ b/packages/smooth_app/lib/l10n/app_fr.arb @@ -1927,6 +1927,26 @@ "@scan_header_compare_button_valid_state_tooltip": { "description": "Tooltip (message visible with a long-press) on the Compare button on top of the scanner, when there is at least two prodiucts" }, + "scan_product_loading": "Vous avez scanné\nle code-barres :", + "@scan_product_loading": { + "description": "Title when a product is loading (carousel card). Please ensure to keep the line break." + }, + "scan_product_loading_initial": "Nous cherchons ce produit !\nMerci de patienter quelques instants…", + "@scan_product_loading_initial": { + "description": "Message when a product is loading (carousel card). Please ensure to keep the line break." + }, + "scan_product_loading_long_request": "Nous cherchons toujours ce produit !\nLe chargement vous paraît long ? Nous aussi…", + "@scan_product_loading_long_request": { + "description": "Message when a product is long to load (carousel card). Please ensure to keep the line break." + }, + "scan_product_loading_unresponsive": "Nous cherchons toujours ce produit !\nVoulez-vous relancer la recherche ?", + "@scan_product_loading_unresponsive": { + "description": "Message when a product is too long to load (carousel card). Please ensure to keep the line break." + }, + "scan_product_loading_restart_button": "Relancer la recherche", + "@scan_product_loading_restart_button": { + "description": "Button to force restart a product search" + }, "portion_calculator_description": "Calculer les valeurs nutritionnelles pour une quantité spécifique", "@portion_calculator_description": { "description": "Sort of title that describes the portion calculator." diff --git a/packages/smooth_app/lib/widgets/smooth_product_carousel.dart b/packages/smooth_app/lib/widgets/smooth_product_carousel.dart index 9846521feb4..3c06ae9c881 100644 --- a/packages/smooth_app/lib/widgets/smooth_product_carousel.dart +++ b/packages/smooth_app/lib/widgets/smooth_product_carousel.dart @@ -178,7 +178,10 @@ class _SmoothProductCarouselState extends State { case ScannedProductState.CACHED: return ScanProductCardLoader(barcode); case ScannedProductState.LOADING: - return SmoothProductCardLoading(barcode: barcode); + return SmoothProductCardLoading( + barcode: barcode, + onRemoveProduct: (_) => _model.removeBarcode(barcode), + ); case ScannedProductState.NOT_FOUND: return SmoothProductCardNotFound( barcode: barcode, diff --git a/packages/smooth_app/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/smooth_app/macos/Flutter/GeneratedPluginRegistrant.swift index fefcf3dac9d..f2845c125d8 100644 --- a/packages/smooth_app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/packages/smooth_app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -13,6 +13,7 @@ import in_app_review import mobile_scanner import package_info_plus import path_provider_foundation +import rive_common import sentry_flutter import share_plus import shared_preferences_foundation @@ -28,6 +29,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + RivePlugin.register(with: registry.registrar(forPlugin: "RivePlugin")) SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/packages/smooth_app/macos/Podfile.lock b/packages/smooth_app/macos/Podfile.lock index c29e0e1cb54..31cce45a894 100644 --- a/packages/smooth_app/macos/Podfile.lock +++ b/packages/smooth_app/macos/Podfile.lock @@ -20,6 +20,8 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - rive_common (0.0.1): + - FlutterMacOS - Sentry/HybridSDK (7.31.5) - sentry_flutter (0.0.1): - Flutter @@ -46,6 +48,7 @@ DEPENDENCIES: - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - rive_common (from `Flutter/ephemeral/.symlinks/plugins/rive_common/macos`) - sentry_flutter (from `Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) @@ -76,6 +79,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos path_provider_foundation: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + rive_common: + :path: Flutter/ephemeral/.symlinks/plugins/rive_common/macos sentry_flutter: :path: Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos share_plus: @@ -98,6 +103,7 @@ SPEC CHECKSUMS: mobile_scanner: ed7618fb749adc6574563e053f3b8e5002c13994 package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 + rive_common: acedcab7802c0ece4b0d838b71d7deb637e1309a Sentry: 4c9babff9034785067c896fd580b1f7de44da020 sentry_flutter: 1346a880b24c0240807b53b10cf50ddad40f504e share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7