diff --git a/packages/smooth_app/assets/fonts/SmoothIcons.ttf b/packages/smooth_app/assets/fonts/SmoothIcons.ttf index a0d1d0587307..7c9566a5b844 100644 Binary files a/packages/smooth_app/assets/fonts/SmoothIcons.ttf and b/packages/smooth_app/assets/fonts/SmoothIcons.ttf differ diff --git a/packages/smooth_app/assets/fonts/icons/config.json b/packages/smooth_app/assets/fonts/icons/config.json index b873269d0aa8..d695a2447d09 100644 --- a/packages/smooth_app/assets/fonts/icons/config.json +++ b/packages/smooth_app/assets/fonts/icons/config.json @@ -1761,6 +1761,48 @@ "search": [ "gallery" ] + }, + { + "uid": "e660fb4e7284d1796f4951980902e4e1", + "css": "ocr", + "code": 59498, + "src": "custom_icons", + "selected": true, + "svg": { + "path": "M152.8 0C68.9 0 0 68.9 0 152.8L0 236.1C-0.2 251.1 7.7 265.1 20.7 272.7 33.6 280.3 49.7 280.3 62.7 272.7 75.7 265.1 83.6 251.1 83.3 236.1L83.3 152.8C83.3 113.9 113.9 83.3 152.8 83.3L236.1 83.3C251.1 83.6 265.1 75.7 272.7 62.7 280.3 49.7 280.3 33.6 272.7 20.7 265.1 7.7 251.1-0.2 236.1 0L152.8 0ZM763.9 0C748.9-0.2 734.9 7.7 727.3 20.7 719.7 33.6 719.7 49.7 727.3 62.7 734.9 75.7 748.9 83.6 763.9 83.3L847.2 83.3C886.1 83.3 916.7 113.9 916.7 152.8L916.7 236.1C916.5 251.1 924.4 265.1 937.3 272.7 950.3 280.3 966.4 280.3 979.4 272.7 992.3 265.1 1000.2 251.1 1000 236.1L1000 152.8C1000 68.9 931.1 0 847.2 0L763.9 0ZM498.2 194.5C480.8 195.2 465.7 206.7 460.3 223.3L345.5 578.2C343.5 582.1 342.2 586.4 341.5 590.8L307.6 695.5C300.5 717.4 312.5 740.9 334.4 748 356.3 755.1 379.8 743.1 386.9 721.1L413.5 638.9 586.5 638.9 613.1 721.1C617.7 735.3 629.5 746 644 749.1 658.6 752.2 673.7 747.3 683.7 736.3 693.7 725.2 697 709.7 692.4 695.5L658.1 589.1C657.4 585.9 656.4 582.7 655 579.7L539.7 223.3C533.9 205.5 516.9 193.7 498.2 194.5ZM500 371.5L559.6 555.6 440.4 555.6 500 371.5ZM41 721.6C18 722-0.3 740.9 0 763.9L0 847.2C0 931.1 68.9 1000 152.8 1000L236.1 1000C251.1 1000.2 265.1 992.3 272.7 979.4 280.3 966.4 280.3 950.3 272.7 937.3 265.1 924.4 251.1 916.5 236.1 916.7L152.8 916.7C113.9 916.7 83.3 886.1 83.3 847.2L83.3 763.9C83.5 752.6 79.1 741.8 71.1 733.8 63.1 725.9 52.3 721.5 41 721.6ZM957.7 721.6C934.7 722 916.3 740.9 916.7 763.9L916.7 847.2C916.7 886.1 886.1 916.7 847.2 916.7L763.9 916.7C748.9 916.5 734.9 924.4 727.3 937.3 719.7 950.3 719.7 966.4 727.3 979.4 734.9 992.3 748.9 1000.2 763.9 1000L847.2 1000C931.1 1000 1000 931.1 1000 847.2L1000 763.9C1000.2 752.6 995.8 741.8 987.8 733.8 979.8 725.9 969 721.5 957.7 721.6Z", + "width": 1000 + }, + "search": [ + "ocr" + ] + }, + { + "uid": "d09a76b7830a5bd5868aa1261b169b00", + "css": "milk_filled_unhappy", + "code": 59518, + "src": "custom_icons", + "selected": true, + "svg": { + "path": "M691.5 0C711.7 0 728 16.4 728 36.6L728 76.7 835.8 292.3 840.1 301.8C846.6 317.8 850 334.9 850 352.3L850 914.6C850 961.3 811.3 1000 764.6 1000L203.7 1000C156.9 1000 118.3 961.3 118.3 914.6L118.3 352.3C118.3 331.5 123.1 310.9 132.4 292.3L240.2 76.7 240.2 36.6C240.2 16.4 256.6 0 276.8 0ZM691.5 167.2L612.5 325C608.3 333.4 606.1 342.8 606.1 352.3L606.1 926.8 764.6 926.8C771.8 926.8 776.8 921.8 776.8 914.6L776.8 352.3C776.8 342.8 774.6 333.5 770.4 325L691.5 167.2ZM247.9 681.1C236.6 691 232.5 706.9 237.6 721 242.7 735.2 256 744.8 271.1 745.2 281 745.4 290.5 741.7 297.6 734.8 335 700.1 385.2 700.1 422.7 734.8 429.6 741.3 438.8 744.9 448.3 744.9 463.4 744.7 476.8 735.3 482.2 721.2 487.5 707.1 483.6 691.2 472.4 681.1 408.9 622.2 311.4 622.2 247.9 681.1ZM276.8 512.2C256.6 512.2 240.2 528.6 240.2 548.8 240.2 569 256.6 585.4 276.8 585.4 297 585.4 313.4 569 313.4 548.8 313.4 528.6 297 512.2 276.8 512.2ZM447.6 512.2C427.3 512.2 411 528.6 411 548.8 411 569 427.3 585.4 447.6 585.4 467.8 585.4 484.1 569 484.1 548.8 484.1 528.6 467.8 512.2 447.6 512.2ZM632.2 122L299.4 122 214 292.7 546.9 292.7 632.2 122Z", + "width": 1000 + }, + "search": [ + "milk_filled_unhappy" + ] + }, + { + "uid": "9e689efdfa6cb85c20cd014ef79d5b26", + "css": "move", + "code": 59519, + "src": "custom_icons", + "selected": true, + "svg": { + "path": "M499.9 0L352.9 145.4C340.9 156.9 335.9 173.9 340.1 190.1 344.2 206.1 356.6 218.8 372.7 223.1 388.7 227.4 405.9 222.6 417.4 210.7L454 174.5 454 338.7C453.7 355.3 462.4 370.6 476.8 379 491 387.4 508.8 387.4 523 379 537.4 370.6 546.1 355.3 545.8 338.7L545.8 174.5 582.4 210.7C594 222.6 611.1 227.4 627.1 223.1 643.2 218.8 655.6 206.1 659.8 190.1 663.9 173.9 658.9 156.9 646.9 145.4L499.9 0ZM821.6 338.9C802.9 338.8 786.1 350 779 367.3 771.8 384.5 775.9 404.4 789.2 417.5L825.4 454.1 661.2 454.1C644.6 453.8 629.3 462.5 620.9 476.9 612.5 491.1 612.5 508.9 620.9 523.1 629.3 537.5 644.6 546.2 661.2 545.9L825.4 545.9 789.2 582.5C777.3 594 772.5 611.2 776.8 627.2 781.1 643.3 793.8 655.7 809.8 659.9 826 664 843 659 854.5 647L999.9 500 854.5 353C845.9 344 834 339 821.6 338.9ZM176.8 339C164.9 339.3 153.6 344.4 145.2 353L0 500 145.2 647C163.1 665.1 192.2 665.2 210.2 647.4 228.3 629.5 228.4 600.5 210.6 582.5L174.4 545.9 338.5 545.9C355.1 546.2 370.5 537.5 378.9 523.1 387.2 508.9 387.2 491.1 378.9 476.9 370.5 462.5 355.1 453.8 338.5 454.1L174.4 454.1 210.6 417.5C224 404.2 228 384.1 220.5 366.7 213.1 349.3 195.7 338.3 176.8 339ZM499.2 614.8C473.9 615.2 453.6 636 454 661.4L454 825.5 417.4 789.4C408.7 780.5 396.8 775.6 384.4 775.6 365.7 775.8 349 787.2 342 804.6 335.1 821.9 339.4 841.7 352.9 854.7L463.5 964C466 967.3 469 970.2 472.3 972.7L499.9 1000 527.4 972.7C530.8 970.2 533.9 967.2 536.5 963.8L646.9 854.7C660.7 841.6 665 821.4 657.8 803.9 650.6 786.4 633.4 775.1 614.4 775.5 602.4 775.7 590.8 780.7 582.4 789.4L545.8 825.5 545.8 661.4C546 648.9 541.1 637 532.4 628.2 523.6 619.4 511.6 614.6 499.2 614.8Z", + "width": 1000 + }, + "search": [ + "move" + ] } ] } \ No newline at end of file diff --git a/packages/smooth_app/assets/fonts/icons/icons.sketch b/packages/smooth_app/assets/fonts/icons/icons.sketch index 662c0597b352..786519d7dd7d 100644 Binary files a/packages/smooth_app/assets/fonts/icons/icons.sketch and b/packages/smooth_app/assets/fonts/icons/icons.sketch differ diff --git a/packages/smooth_app/assets/fonts/icons/milk_filled_unhappy.svg b/packages/smooth_app/assets/fonts/icons/milk_filled_unhappy.svg new file mode 100644 index 000000000000..ad77cf13daa5 --- /dev/null +++ b/packages/smooth_app/assets/fonts/icons/milk_filled_unhappy.svg @@ -0,0 +1,9 @@ + + + Milk unhappy + + + + + + \ No newline at end of file diff --git a/packages/smooth_app/assets/fonts/icons/move.svg b/packages/smooth_app/assets/fonts/icons/move.svg new file mode 100644 index 000000000000..2c2b164bd29a --- /dev/null +++ b/packages/smooth_app/assets/fonts/icons/move.svg @@ -0,0 +1,7 @@ + + + Move + + + + \ No newline at end of file diff --git a/packages/smooth_app/assets/fonts/icons/ocr.svg b/packages/smooth_app/assets/fonts/icons/ocr.svg new file mode 100644 index 000000000000..75622552ae6f --- /dev/null +++ b/packages/smooth_app/assets/fonts/icons/ocr.svg @@ -0,0 +1,9 @@ + + + Gallery + + + + + + \ No newline at end of file diff --git a/packages/smooth_app/lib/cards/product_cards/product_title_card.dart b/packages/smooth_app/lib/cards/product_cards/product_title_card.dart index c92bc616e1df..fb712536e18d 100644 --- a/packages/smooth_app/lib/cards/product_cards/product_title_card.dart +++ b/packages/smooth_app/lib/cards/product_cards/product_title_card.dart @@ -5,6 +5,7 @@ import 'package:provider/provider.dart'; import 'package:smooth_app/cards/product_cards/smooth_product_base_card.dart'; import 'package:smooth_app/cards/product_cards/smooth_product_image.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; +import 'package:smooth_app/generic_lib/widgets/picture_not_found.dart'; import 'package:smooth_app/helpers/product_cards_helper.dart'; import 'package:smooth_app/pages/product/gallery_view/product_image_gallery_view.dart'; @@ -83,6 +84,7 @@ class ProductTitleCard extends StatelessWidget { imageNotFoundBorder: 1.0, heroTag: heroTag, borderRadius: BorderRadius.circular(14.0), + noImageBuilder: (_) => const PictureNotFound(), onTap: !dense ? () async => Navigator.push( context, 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 e5cbe6e95d0c..f1a4b1d1502c 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 @@ -34,6 +34,7 @@ class ProductPicture extends StatefulWidget { double imageFoundBorder = 0.0, double imageNotFoundBorder = 0.0, TextStyle? errorTextStyle, + WidgetBuilder? noImageBuilder, }) : this._( transientFile: null, product: product, @@ -49,6 +50,7 @@ class ProductPicture extends StatefulWidget { errorTextStyle: errorTextStyle, showObsoleteIcon: showObsoleteIcon, showOwnerIcon: showOwnerIcon, + noImageBuilder: noImageBuilder, ); ProductPicture.fromTransientFile({ @@ -66,6 +68,7 @@ class ProductPicture extends StatefulWidget { double imageFoundBorder = 0.0, double imageNotFoundBorder = 0.0, TextStyle? errorTextStyle, + WidgetBuilder? noImageBuilder, }) : this._( transientFile: transientFile, product: product, @@ -81,6 +84,7 @@ class ProductPicture extends StatefulWidget { errorTextStyle: errorTextStyle, showObsoleteIcon: showObsoleteIcon, showOwnerIcon: showOwnerIcon, + noImageBuilder: noImageBuilder, ); ProductPicture._({ @@ -98,6 +102,7 @@ class ProductPicture extends StatefulWidget { this.errorTextStyle, this.showObsoleteIcon = false, this.showOwnerIcon = false, + this.noImageBuilder, super.key, }) : assert(imageFoundBorder >= 0.0), assert(imageNotFoundBorder >= 0.0), @@ -129,6 +134,9 @@ class ProductPicture extends StatefulWidget { /// Style when there is no image/an error final TextStyle? errorTextStyle; + /// Allows to change the placeholder + final WidgetBuilder? noImageBuilder; + @override State createState() => _ProductPictureState(); @@ -202,9 +210,12 @@ class _ProductPictureState extends State { child = _ProductPictureAssetsSvg( asset: 'assets/product/product_not_found_text.svg', + imageOverride: widget.noImageBuilder, semanticsLabel: appLocalizations .product_page_image_no_image_available_accessibility_label, - text: appLocalizations.product_page_image_no_image_available, + text: widget.noImageBuilder == null + ? appLocalizations.product_page_image_no_image_available + : null, textStyle: TextStyle( color: context.extension().primaryDark, ).merge(widget.errorTextStyle ?? const TextStyle()), @@ -578,6 +589,7 @@ class _ProductPictureAssetsSvg extends StatelessWidget { required this.textStyle, required this.size, required this.child, + this.imageOverride, this.borderRadius, this.border = 0.0, }) : assert(asset.isNotEmpty), @@ -591,6 +603,7 @@ class _ProductPictureAssetsSvg extends StatelessWidget { final Widget? child; final BorderRadius? borderRadius; final double border; + final WidgetBuilder? imageOverride; @override Widget build(BuildContext context) { @@ -603,12 +616,13 @@ class _ProductPictureAssetsSvg extends StatelessWidget { child: Stack( children: [ Positioned.fill( - child: SvgPicture.asset( - asset, - width: size.width, - height: size.height, - fit: BoxFit.cover, - ), + child: imageOverride?.call(context) ?? + SvgPicture.asset( + asset, + width: size.width, + height: size.height, + fit: BoxFit.cover, + ), ), if (text != null) Padding( diff --git a/packages/smooth_app/lib/generic_lib/bottom_sheets/smooth_bottom_sheet.dart b/packages/smooth_app/lib/generic_lib/bottom_sheets/smooth_bottom_sheet.dart index f4e6dc2a3e94..cbc3bf628293 100644 --- a/packages/smooth_app/lib/generic_lib/bottom_sheets/smooth_bottom_sheet.dart +++ b/packages/smooth_app/lib/generic_lib/bottom_sheets/smooth_bottom_sheet.dart @@ -182,6 +182,8 @@ class SmoothModalSheet extends StatelessWidget { required this.body, bool prefixIndicator = false, bool closeButton = true, + Color? headerBackgroundColor, + Color? headerForegroundColor, this.bodyPadding, this.expandBody = false, double? closeButtonSemanticsOrder, @@ -195,6 +197,8 @@ class SmoothModalSheet extends StatelessWidget { semanticsOrder: closeButtonSemanticsOrder, ) : null, + backgroundColor: headerBackgroundColor, + foregroundColor: headerForegroundColor, ); final SmoothModalSheetHeader header; @@ -443,7 +447,7 @@ class SmoothModalSheetHeaderCloseButton extends StatelessWidget ), ), margin: const EdgeInsetsDirectional.all(VERY_SMALL_SPACE), - padding: const EdgeInsetsDirectional.all(SMALL_SPACE), + padding: const EdgeInsetsDirectional.all(6.0), child: const icons.Close( size: 13.0, ), @@ -467,13 +471,16 @@ class SmoothModalSheetHeaderCloseButton extends StatelessWidget child: Tooltip( message: MaterialLocalizations.of(context).closeButtonTooltip, enableFeedback: true, - child: InkWell( - onTap: () { - SmoothHapticFeedback.click(); - Navigator.of(context).pop(); - }, - customBorder: const CircleBorder(), - child: icon, + child: Material( + type: MaterialType.transparency, + child: InkWell( + onTap: () { + SmoothHapticFeedback.click(); + Navigator.of(context).pop(); + }, + customBorder: const CircleBorder(), + child: icon, + ), ), ), ), @@ -521,3 +528,33 @@ abstract class SizeWidget implements Widget { bool get requiresPadding; } + +/// With a [SmoothModalSheet], if you want to display simple things (eg: text), +/// you can use this widget +class SmoothModalSheetBodyContainer extends StatelessWidget { + const SmoothModalSheetBodyContainer({ + required this.child, + super.key, + }); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsetsDirectional.only( + start: MEDIUM_SPACE, + end: MEDIUM_SPACE, + top: VERY_SMALL_SPACE, + bottom: VERY_SMALL_SPACE + MediaQuery.viewPaddingOf(context).bottom, + ), + child: DefaultTextStyle.merge( + style: const TextStyle( + fontSize: 15.0, + height: 1.7, + ), + child: child, + ), + ); + } +} diff --git a/packages/smooth_app/lib/generic_lib/widgets/picture_not_found.dart b/packages/smooth_app/lib/generic_lib/widgets/picture_not_found.dart index ddcd5100431a..9f2bfbfb30db 100644 --- a/packages/smooth_app/lib/generic_lib/widgets/picture_not_found.dart +++ b/packages/smooth_app/lib/generic_lib/widgets/picture_not_found.dart @@ -1,19 +1,62 @@ -import 'package:flutter/widgets.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:smooth_app/helpers/app_helper.dart'; +import 'package:flutter/material.dart'; +import 'package:smooth_app/resources/app_icons.dart' as icons; -/// Displays a default asset as a _picture not found_ image. class PictureNotFound extends StatelessWidget { - const PictureNotFound({this.boxFit = BoxFit.cover}); + const PictureNotFound({ + this.backgroundColor, + this.foregroundColor, + super.key, + }) : _useInk = false, + backgroundDecoration = null; - final BoxFit boxFit; + const PictureNotFound.ink({ + this.backgroundDecoration, + this.foregroundColor, + super.key, + }) : _useInk = true, + backgroundColor = null; - static const String _notFoundAsset = 'assets/product/product_not_found.svg'; + final BoxDecoration? backgroundDecoration; + final Color? backgroundColor; + final Color? foregroundColor; + final bool _useInk; @override - Widget build(BuildContext context) => SvgPicture.asset( - _notFoundAsset, - fit: boxFit, - package: AppHelper.APP_PACKAGE, + Widget build(BuildContext context) { + final Widget child = SizedBox.expand( + child: FractionallySizedBox( + widthFactor: 0.5, + heightFactor: 0.5, + child: FittedBox( + child: icons.Milk.unhappy( + color: foregroundColor ?? const Color(0xFF949494), + size: 1000, + ), + ), + ), + ); + + if (_useInk) { + return Ink( + decoration: BoxDecoration( + color: backgroundColor ?? const Color(0xFFE5E5E5), + ).copyWith( + color: backgroundDecoration?.color, + image: backgroundDecoration?.image, + border: backgroundDecoration?.border, + borderRadius: backgroundDecoration?.borderRadius, + boxShadow: backgroundDecoration?.boxShadow, + gradient: backgroundDecoration?.gradient, + backgroundBlendMode: backgroundDecoration?.backgroundBlendMode, + shape: BoxShape.rectangle, + ), + child: child, + ); + } else { + return ColoredBox( + color: backgroundColor ?? const Color(0xFFE5E5E5), + child: child, ); + } + } } diff --git a/packages/smooth_app/lib/generic_lib/widgets/smooth_card.dart b/packages/smooth_app/lib/generic_lib/widgets/smooth_card.dart index 038f79489bf8..8c46469a9087 100644 --- a/packages/smooth_app/lib/generic_lib/widgets/smooth_card.dart +++ b/packages/smooth_app/lib/generic_lib/widgets/smooth_card.dart @@ -113,6 +113,7 @@ class SmoothCardWithRoundedHeader extends StatelessWidget { this.titleTextStyle, this.titlePadding, this.contentPadding, + this.contentBackgroundColor, }); final String title; @@ -123,6 +124,7 @@ class SmoothCardWithRoundedHeader extends StatelessWidget { final TextStyle? titleTextStyle; final EdgeInsetsGeometry? titlePadding; final EdgeInsetsGeometry? contentPadding; + final Color? contentBackgroundColor; @override Widget build(BuildContext context) { @@ -130,84 +132,105 @@ class SmoothCardWithRoundedHeader extends StatelessWidget { final SmoothColorsThemeExtension extension = context.extension(); - final Color color = - context.lightTheme() ? extension.primaryBlack : Colors.black; - - return Column( - children: [ - Semantics( - label: title, - excludeSemantics: true, - child: CustomPaint( - painter: _SmoothCardWithRoundedHeaderBackgroundPainter( - color: color, - radius: ROUNDED_RADIUS, - ), - child: Padding( - padding: titlePadding ?? - const EdgeInsetsDirectional.symmetric( - vertical: BALANCED_SPACE, - horizontal: LARGE_SPACE, - ), - child: Row( - children: [ - if (leading != null) - IconTheme( - data: IconThemeData( - color: color, - size: 17.0, - ), - child: DecoratedBox( - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, + final Color color = getHeaderColor(context); + + return DecoratedBox( + decoration: const BoxDecoration( + borderRadius: ROUNDED_BORDER_RADIUS, + boxShadow: [ + BoxShadow( + color: Color(0x03000000), + blurRadius: 2.0, + spreadRadius: 0.0, + offset: Offset(0.0, 2.0), + ), + ], + ), + child: Column( + children: [ + Semantics( + label: title, + excludeSemantics: true, + child: CustomPaint( + painter: _SmoothCardWithRoundedHeaderBackgroundPainter( + color: color, + radius: ROUNDED_RADIUS, + ), + child: Padding( + padding: titlePadding ?? + const EdgeInsetsDirectional.symmetric( + vertical: BALANCED_SPACE, + horizontal: LARGE_SPACE, + ), + child: Row( + children: [ + if (leading != null) + IconTheme( + data: IconThemeData( + color: color, + size: 17.0, ), - child: Padding( - padding: const EdgeInsetsDirectional.all(6.0), - child: leading, + child: DecoratedBox( + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: Padding( + padding: const EdgeInsetsDirectional.all(6.0), + child: leading, + ), ), ), - ), - const SizedBox(width: MEDIUM_SPACE), - Expanded( - child: Text( - title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: - (titleTextStyle ?? themeData.textTheme.displaySmall) - ?.copyWith( - color: Colors.white, - ), - ), - ), - if (trailing != null) ...[ const SizedBox(width: MEDIUM_SPACE), - IconTheme( - data: const IconThemeData( - color: Colors.white, - size: 20.0, + Expanded( + child: Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: + (titleTextStyle ?? themeData.textTheme.displaySmall) + ?.copyWith( + color: Colors.white, + ), ), - child: trailing!, ), + if (trailing != null) ...[ + const SizedBox(width: MEDIUM_SPACE), + IconTheme( + data: const IconThemeData( + color: Colors.white, + size: 20.0, + ), + child: trailing!, + ), + ], ], - ], + ), ), ), ), - ), - SmoothCard( - margin: EdgeInsets.zero, - padding: contentPadding ?? - const EdgeInsetsDirectional.only( - top: MEDIUM_SPACE, - ), - color: context.darkTheme() ? extension.primaryUltraBlack : null, - child: child, - ), - ], + SmoothCard( + margin: EdgeInsets.zero, + padding: contentPadding ?? + const EdgeInsetsDirectional.only( + top: MEDIUM_SPACE, + ), + color: contentBackgroundColor ?? + (context.darkTheme() ? extension.primaryUltraBlack : null), + child: child, + ), + ], + ), ); } + + static Color getHeaderColor(BuildContext context) { + final SmoothColorsThemeExtension extension = + context.extension(); + return context.lightTheme(listen: false) + ? extension.primaryBlack + : Colors.black; + } } /// We need this [CustomPainter] to draw the background below the other card @@ -249,3 +272,41 @@ class _SmoothCardWithRoundedHeaderBackgroundPainter extends CustomPainter { ) => false; } + +class SmoothCardHeaderButton extends StatelessWidget { + const SmoothCardHeaderButton({ + required this.tooltip, + required this.child, + required this.onTap, + this.padding, + super.key, + }); + + final String tooltip; + final Widget child; + final VoidCallback onTap; + final EdgeInsetsGeometry? padding; + + @override + Widget build(BuildContext context) { + return Material( + type: MaterialType.transparency, + child: Semantics( + label: tooltip, + button: true, + excludeSemantics: true, + child: Tooltip( + message: tooltip, + child: InkWell( + customBorder: const CircleBorder(), + onTap: onTap, + child: Padding( + padding: padding ?? const EdgeInsetsDirectional.all(MEDIUM_SPACE), + child: child, + ), + ), + ), + ), + ); + } +} diff --git a/packages/smooth_app/lib/helpers/ui_helpers.dart b/packages/smooth_app/lib/helpers/ui_helpers.dart index 7ffeb38743a9..eeb4a803502f 100644 --- a/packages/smooth_app/lib/helpers/ui_helpers.dart +++ b/packages/smooth_app/lib/helpers/ui_helpers.dart @@ -61,10 +61,15 @@ extension StatelessWidgetExtension on StatelessWidget { } extension StateExtension on State { - void onNextFrame(VoidCallback callback) { - WidgetsBinding.instance.addPostFrameCallback((_) { + void onNextFrame(VoidCallback callback, {bool forceRedraw = false}) { + final WidgetsBinding binding = WidgetsBinding.instance; + binding.addPostFrameCallback((_) { callback(); }); + + if (forceRedraw) { + binding.ensureVisualUpdate(); + } } } diff --git a/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_world_map_card.dart b/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_world_map_card.dart index 7fbbac5a7bfb..2cd0f71c9054 100644 --- a/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_world_map_card.dart +++ b/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_world_map_card.dart @@ -8,6 +8,7 @@ import 'package:provider/provider.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/pages/product/world_map_page.dart'; import 'package:smooth_app/resources/app_icons.dart' as icons; +import 'package:smooth_app/widgets/smooth_indicator_icon.dart'; class KnowledgePanelWorldMapCard extends StatelessWidget { const KnowledgePanelWorldMapCard(this.mapElement); @@ -177,24 +178,7 @@ class _ExpandMapIcon extends StatelessWidget { Widget build(BuildContext context) { return const Align( alignment: AlignmentDirectional.bottomEnd, - child: Padding( - padding: EdgeInsetsDirectional.all( - VERY_SMALL_SPACE, - ), - child: DecoratedBox( - decoration: BoxDecoration( - color: Colors.black38, - shape: BoxShape.circle, - ), - child: Padding( - padding: EdgeInsetsDirectional.all(SMALL_SPACE), - child: icons.Expand( - size: 15.0, - color: Colors.white, - ), - ), - ), - ), + child: SmoothIndicatorIcon(icon: icons.Expand()), ); } } diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index 024b534a361a..950c5651882c 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -1591,6 +1591,22 @@ "@edit_product_form_save": { "description": "Product edition - Nutrition facts - Save button" }, + "edit_product_ingredients_photo_title": "Ingredients photo", + "@edit_product_ingredients_photo_title": { + "description": "Product edition - Ingredients - Title" + }, + "edit_product_ingredients_list_title": "List of ingredients", + "@edit_product_ingredients_list_title": { + "description": "Product edition - Ingredients - Title" + }, + "edit_product_packaging_photo_title": "Packaging photo", + "@edit_product_packaging_photo_title": { + "description": "Product edition - Packaging - Title" + }, + "edit_product_packaging_list_title": "Packaging list", + "@edit_product_packaging_list_title": { + "description": "Product edition - Packaging - Title" + }, "no_data_available": "No data available", "@no_data_available": { "description": "When there are no data to display" @@ -1606,6 +1622,10 @@ "@edit_ingredients_extract_ingredients_btn_text": { "description": "Ingredients edition - Extract ingredients" }, + "edit_ingredients_extract_ingredients_btn_text_short": "Extract ingredients", + "@edit_ingredients_extract_ingredients_btn_text_short": { + "description": "Ingredients edition - Extract ingredients (short label)" + }, "edit_ingredients_extracting_ingredients_btn_text": "Extracting ingredients\nfrom the photo", "@edit_ingredients_extracting_ingredients_btn_text": { "description": "Ingredients edition - Extracting ingredients" @@ -1630,6 +1650,10 @@ "@edit_packaging_extract_btn_text": { "description": "Packaging edition - OCR-Extract packaging" }, + "edit_packaging_extract_btn_text_short": "Extract packaging", + "@edit_packaging_extract_btn_text_short": { + "description": "Packaging edition - OCR-Extract packaging (short label)" + }, "edit_packaging_extracting_btn_text": "Extracting packaging from the photo", "@edit_packaging_extracting_btn_text": { "description": "Packaging edition - OCR-Extracting packaging" @@ -1654,6 +1678,14 @@ "@edit_ocr_extract_failed": { "description": "OCR extraction - message for failed" }, + "edit_ocr_extract_disabled_title": "No picture!", + "@edit_ocr_extract_disabled_title": { + "description": "OCR extraction - title for disabled" + }, + "edit_ocr_extract_disabled_message": "In order to use the text extraction feature, you must first take a photo.", + "@edit_ocr_extract_disabled_message": { + "description": "OCR extraction - message for disabled" + }, "user_list_dialog_new_title": "New list of products", "@user_list_dialog_new_title": { "description": "Title of the 'new user list' dialog" diff --git a/packages/smooth_app/lib/l10n/app_fr.arb b/packages/smooth_app/lib/l10n/app_fr.arb index d84e495eedac..ea9966d0d306 100644 --- a/packages/smooth_app/lib/l10n/app_fr.arb +++ b/packages/smooth_app/lib/l10n/app_fr.arb @@ -1588,6 +1588,22 @@ "@edit_product_form_save": { "description": "Product edition - Nutrition facts - Save button" }, + "edit_product_ingredients_photo_title": "Photo des ingrédients", + "@edit_product_ingredients_photo_title": { + "description": "Product edition - Ingredients - Title" + }, + "edit_product_ingredients_list_title": "Liste des ingrédients", + "@edit_product_ingredients_list_title": { + "description": "Product edition - Ingredients - Title" + }, + "edit_product_packaging_photo_title": "Photo du packaging", + "@edit_product_packaging_photo_title": { + "description": "Product edition - Packaging - Title" + }, + "edit_product_packaging_list_title": "Détails du Packaging", + "@edit_product_packaging_list_title": { + "description": "Product edition - Packaging - Title" + }, "no_data_available": "Aucune donnée disponible", "@no_data_available": { "description": "When there are no data to display" @@ -1603,6 +1619,10 @@ "@edit_ingredients_extract_ingredients_btn_text": { "description": "Ingredients edition - Extract ingredients" }, + "edit_ingredients_extract_ingredients_btn_text_short": "Extraire les ingrédients", + "@edit_ingredients_extract_ingredients_btn_text_short": { + "description": "Ingredients edition - Extract ingredients (short label)" + }, "edit_ingredients_extracting_ingredients_btn_text": "Extraction des ingrédients à partir de la photo", "@edit_ingredients_extracting_ingredients_btn_text": { "description": "Ingredients edition - Extracting ingredients" @@ -1623,10 +1643,14 @@ "@edit_ingredients_refresh_photo_btn_text": { "description": "Ingredients edition - Refresh photo" }, - "edit_packaging_extract_btn_text": "Extraire l'emballage", + "edit_packaging_extract_btn_text": "Extraire l'emballage de la photo", "@edit_packaging_extract_btn_text": { "description": "Packaging edition - OCR-Extract packaging" }, + "edit_packaging_extract_btn_text_short": "Extraire l'emballage", + "@edit_packaging_extract_btn_text_short": { + "description": "Packaging edition - OCR-Extract packaging (short label)" + }, "edit_packaging_extracting_btn_text": "Extraction de l'emballage à partir de la photo", "@edit_packaging_extracting_btn_text": { "description": "Packaging edition - OCR-Extracting packaging" @@ -1651,6 +1675,14 @@ "@edit_ocr_extract_failed": { "description": "OCR extraction - message for failed" }, + "edit_ocr_extract_disabled_title": "Photo manquante !", + "@edit_ocr_extract_disabled_title": { + "description": "OCR extraction - title for disabled" + }, + "edit_ocr_extract_disabled_message": "Afin de pouvoir utiliser l'extraction du texte, vous devez d'abord prendre une photo.", + "@edit_ocr_extract_disabled_message": { + "description": "OCR extraction - message for disabled" + }, "user_list_dialog_new_title": "Nouvelle liste de produits", "@user_list_dialog_new_title": { "description": "Title of the 'new user list' dialog" diff --git a/packages/smooth_app/lib/pages/image/product_image_other_page.dart b/packages/smooth_app/lib/pages/image/product_image_other_page.dart index 8f9e27d913aa..913dfcbf1f8e 100644 --- a/packages/smooth_app/lib/pages/image/product_image_other_page.dart +++ b/packages/smooth_app/lib/pages/image/product_image_other_page.dart @@ -85,7 +85,7 @@ class ProductImageOtherPage extends StatefulWidget { values: imageFields, prefixIcons: imageFields.map((final ImageField imageField) { return switch (imageField) { - ImageField.FRONT => const icons.Milk.filled(), + ImageField.FRONT => const icons.Milk.happy(), ImageField.INGREDIENTS => const icons.Ingredients.alt(), ImageField.NUTRITION => const icons.NutritionFacts(), ImageField.PACKAGING => const icons.Recycling(), 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 554ad7bd2a18..41c25e50ae07 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 @@ -92,19 +92,25 @@ class _SmoothAutocompleteTextFieldState ], style: widget.textStyle, decoration: InputDecoration( - suffixIcon: widget.suffixIcon, - filled: true, - border: OutlineInputBorder( - borderRadius: widget.borderRadius ?? ANGULAR_BORDER_RADIUS, - borderSide: BorderSide.none, - ), contentPadding: widget.padding ?? const EdgeInsets.symmetric( horizontal: SMALL_SPACE, vertical: SMALL_SPACE, ), - hintText: widget.hintText, + suffixIcon: widget.suffixIcon, + filled: true, hintStyle: SmoothTextFormField.defaultHintTextStyle(context), + hintText: widget.hintText, + border: OutlineInputBorder( + borderRadius: widget.borderRadius ?? ANGULAR_BORDER_RADIUS, + ), + enabledBorder: OutlineInputBorder( + borderRadius: widget.borderRadius ?? CIRCULAR_BORDER_RADIUS, + borderSide: const BorderSide( + color: Colors.transparent, + width: 5.0, + ), + ), suffix: Offstage( offstage: !_loading, child: SizedBox( diff --git a/packages/smooth_app/lib/pages/product/add_basic_details_page.dart b/packages/smooth_app/lib/pages/product/add_basic_details_page.dart index d8074f70b26d..56aabbd2d8c9 100644 --- a/packages/smooth_app/lib/pages/product/add_basic_details_page.dart +++ b/packages/smooth_app/lib/pages/product/add_basic_details_page.dart @@ -53,7 +53,7 @@ class _AddBasicDetailsPageState extends State { late final TextEditingControllerWithHistory _brandNameController; late final TextEditingControllerWithHistory _weightController; - final double _heightSpace = MEDIUM_SPACE; + final double _heightSpace = LARGE_SPACE; final GlobalKey _formKey = GlobalKey(); late final Product _product; @@ -424,7 +424,7 @@ class _ProductMultilingualNameInputWidget extends StatelessWidget { return _BasicDetailInputWrapper( title: appLocalizations.product_name, - icon: const icons.Milk.filled(), + icon: const icons.Milk.happy(), ownerField: ownerField, contentPadding: const EdgeInsetsDirectional.only( bottom: MEDIUM_SPACE, diff --git a/packages/smooth_app/lib/pages/product/edit_ocr/edit_ocr_image.dart b/packages/smooth_app/lib/pages/product/edit_ocr/edit_ocr_image.dart new file mode 100644 index 000000000000..bb7219556b08 --- /dev/null +++ b/packages/smooth_app/lib/pages/product/edit_ocr/edit_ocr_image.dart @@ -0,0 +1,599 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; +import 'package:smooth_app/database/transient_file.dart'; +import 'package:smooth_app/generic_lib/bottom_sheets/smooth_bottom_sheet.dart'; +import 'package:smooth_app/generic_lib/design_constants.dart'; +import 'package:smooth_app/generic_lib/widgets/picture_not_found.dart'; +import 'package:smooth_app/generic_lib/widgets/smooth_card.dart'; +import 'package:smooth_app/helpers/num_utils.dart'; +import 'package:smooth_app/pages/image/product_image_helper.dart'; +import 'package:smooth_app/pages/product/edit_ocr/edit_ocr_page.dart'; +import 'package:smooth_app/pages/product/edit_ocr/edit_ocr_textfield.dart'; +import 'package:smooth_app/pages/product/edit_ocr/ocr_helper.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_indicator_icon.dart'; + +class EditOCRImageWidget extends StatelessWidget { + const EditOCRImageWidget({ + required this.helper, + required this.transientFile, + required this.ownerField, + required this.onTakePicture, + required this.onEditImage, + required this.onExtractText, + }); + + final OcrHelper helper; + final TransientFile transientFile; + final bool ownerField; + + final VoidCallback onTakePicture; + final VoidCallback onEditImage; + final VoidCallback onExtractText; + + @override + Widget build(BuildContext context) { + final SmoothColorsThemeExtension extension = + context.extension(); + final bool lightTheme = context.lightTheme(); + + final AppLocalizations appLocalizations = AppLocalizations.of(context); + + final ImageProvider? imageProvider = transientFile.getImageProvider(); + final bool hasImage = imageProvider != null; + + final Size screenSize = MediaQuery.sizeOf(context); + final double height = screenSize.height; + + Widget child; + Widget? headerIcons; + + /// If the ownerField icon is visible, we need to reduce the header size + /// (this icon already contains the padding) + bool reduceHeader = false; + + if (hasImage) { + child = SizedBox( + height: height, + child: _EditOCRImageFound( + imageProvider: imageProvider, + ), + ); + + if (transientFile.expired) { + headerIcons = Tooltip( + message: appLocalizations.product_image_outdated_message, + textAlign: TextAlign.center, + child: DecoratedBox( + decoration: BoxDecoration( + color: extension.warning, + borderRadius: ROUNDED_BORDER_RADIUS, + ), + child: const Padding( + padding: EdgeInsetsDirectional.only( + top: 6.5, + bottom: 7.5, + start: 7.0, + end: 7.0, + ), + child: icons.Outdated(size: 15.0), + ), + ), + ); + } + + if (ownerField) { + reduceHeader = true; + + if (headerIcons == null) { + headerIcons = const EditOCROwnerFieldIcon(); + } else { + headerIcons = Row( + mainAxisSize: MainAxisSize.min, + children: [ + const EditOCROwnerFieldIcon(), + headerIcons, + ], + ); + } + } + } else { + child = _EditOCRImageNotFound( + onTap: onTakePicture, + ); + } + + return SmoothCardWithRoundedHeader( + title: helper.getPhotoTitle(appLocalizations), + leading: const icons.Camera.happy(), + trailing: headerIcons, + titlePadding: reduceHeader + ? const EdgeInsetsDirectional.symmetric( + vertical: 2.0, + horizontal: LARGE_SPACE, + ) + : null, + contentPadding: EdgeInsets.zero, + contentBackgroundColor: lightTheme ? extension.primaryBlack : null, + child: ConstrainedBox( + constraints: BoxConstraints( + minWidth: double.infinity, + maxWidth: double.infinity, + minHeight: height * 0.3, + maxHeight: height * 0.35, + ), + child: Column( + children: [ + Expanded( + child: ClipRRect( + borderRadius: ROUNDED_BORDER_RADIUS, + child: child, + ), + ), + _EditOCRImageActions( + helper: helper, + hasImage: hasImage, + onTakePicture: onTakePicture, + onEditImage: onEditImage, + onExtractText: onExtractText, + ), + ], + ), + ), + ); + } +} + +class _EditOCRImageFound extends StatelessWidget { + const _EditOCRImageFound({ + required this.imageProvider, + }); + + final ImageProvider imageProvider; + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: ROUNDED_BORDER_RADIUS, + child: Consumer( + builder: (BuildContext context, OcrState state, _) { + return Stack( + children: [ + Positioned.fill( + child: ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0), + child: Image( + fit: BoxFit.cover, + image: imageProvider, + opacity: AlwaysStoppedAnimation( + context.lightTheme() ? 0.3 : 0.55, + ), + ), + ), + ), + Positioned.fill( + child: AbsorbPointer( + absorbing: state == OcrState.EXTRACTING_DATA, + child: InteractiveViewer( + interactionEndFrictionCoefficient: double.infinity, + minScale: 0.1, + maxScale: 5.0, + child: Image( + fit: BoxFit.contain, + image: imageProvider, + ), + ), + ), + ), + if (state == OcrState.IMAGE_LOADED) + const Align( + alignment: AlignmentDirectional.bottomEnd, + child: ExcludeSemantics( + child: SmoothIndicatorIcon( + icon: icons.Move(), + ), + ), + ) + else if (state == OcrState.IMAGE_LOADING) + const Center( + child: CloudUploadAnimation.circle(size: 65.0), + ) + else if (state == OcrState.EXTRACTING_DATA) + Builder(builder: (BuildContext context) { + final SmoothColorsThemeExtension extension = + context.extension(); + + return Positioned.fill( + child: _ExtractTextAnimation( + tintColor: extension.secondaryNormal, + tintColorGradient: extension.secondaryLight, + ), + ); + }), + ], + ); + }, + ), + ); + } +} + +class _EditOCRImageNotFound extends StatelessWidget { + const _EditOCRImageNotFound({ + required this.onTap, + }); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return InkWell( + borderRadius: ROUNDED_BORDER_RADIUS, + onTap: onTap, + child: const Stack( + children: [ + Positioned.fill( + child: PictureNotFound.ink( + backgroundDecoration: BoxDecoration( + borderRadius: ROUNDED_BORDER_RADIUS, + ), + ), + ), + Align( + alignment: AlignmentDirectional.bottomEnd, + child: ExcludeSemantics( + child: SmoothIndicatorIcon( + icon: Icon(Icons.add_a_photo_rounded), + ), + ), + ), + ], + ), + ); + } +} + +class _EditOCRImageActions extends StatelessWidget { + const _EditOCRImageActions({ + required this.helper, + required this.hasImage, + required this.onTakePicture, + required this.onEditImage, + required this.onExtractText, + }); + + final OcrHelper helper; + final bool hasImage; + final VoidCallback onTakePicture; + final VoidCallback onEditImage; + final VoidCallback onExtractText; + + @override + Widget build(BuildContext context) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + + return Padding( + padding: const EdgeInsetsDirectional.only( + top: BALANCED_SPACE, + start: LARGE_SPACE, + end: LARGE_SPACE, + bottom: MEDIUM_SPACE, + ), + child: Consumer( + builder: (BuildContext context, OcrState ocrState, _) { + return Column( + children: [ + _extractTextButton( + context, + appLocalizations, + ocrState, + ), + const SizedBox(height: BALANCED_SPACE), + if (hasImage) + _editPictureButton(appLocalizations, ocrState) + else + _takePictureButton(appLocalizations, ocrState), + ], + ); + }, + ), + ); + } + + _EditOCRImageButton _extractTextButton( + BuildContext context, + AppLocalizations appLocalizations, + OcrState state, + ) { + final VoidCallback onTap; + + if (!hasImage) { + onTap = () => _onImageUnavailable(context); + } else if (state == OcrState.IMAGE_LOADING) { + onTap = () => _onImageUpload(context); + } else { + onTap = onExtractText; + } + + return _EditOCRImageButton( + label: helper.getActionExtractShortText(appLocalizations), + icon: const icons.OCR( + size: 18.0, + ), + onPressed: onTap, + enabled: hasImage && state == OcrState.IMAGE_LOADED, + ); + } + + _EditOCRImageButton _editPictureButton( + AppLocalizations appLocalizations, + OcrState state, + ) { + return _EditOCRImageButton( + label: appLocalizations.product_edit_photo_title, + icon: const icons.Edit( + size: 16.0, + ), + onPressed: state != OcrState.EXTRACTING_DATA ? onEditImage : null, + ); + } + + _EditOCRImageButton _takePictureButton( + AppLocalizations appLocalizations, + OcrState state, + ) { + return _EditOCRImageButton( + label: appLocalizations.edit_product_action_take_picture, + icon: const Icon( + Icons.add_a_photo_rounded, + size: 18.0, + ), + padding: const EdgeInsetsDirectional.only( + start: VERY_SMALL_SPACE, + end: 6.0, + top: SMALL_SPACE, + bottom: SMALL_SPACE, + ), + onPressed: state != OcrState.EXTRACTING_DATA ? onTakePicture : null, + ); + } + + Future _onImageUnavailable(BuildContext context) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + + return showSmoothModalSheet( + context: context, + builder: (BuildContext context) { + return SmoothModalSheet( + title: appLocalizations.edit_ocr_extract_disabled_title, + body: SmoothModalSheetBodyContainer( + child: Text(appLocalizations.edit_ocr_extract_disabled_message), + ), + ); + }, + ); + } + + Future _onImageUpload(BuildContext context) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + + return showSmoothModalSheet( + context: context, + builder: (BuildContext context) { + return SmoothModalSheet( + title: helper.getActionLoadingPhotoDialogTitle(appLocalizations), + body: SmoothModalSheetBodyContainer( + child: Text( + helper.getActionLoadingPhotoDialogBody(appLocalizations), + ), + ), + ); + }, + ); + } +} + +class _EditOCRImageButton extends StatelessWidget { + const _EditOCRImageButton({ + required this.label, + required this.icon, + required this.onPressed, + this.enabled = true, + this.padding, + }); + + final String label; + final Widget icon; + final VoidCallback? onPressed; + final bool enabled; + final EdgeInsetsGeometry? padding; + + @override + Widget build(BuildContext context) { + final Color color = + (onPressed != null && enabled) ? Colors.white : const Color(0x88FFFFFF); + + return Ink( + decoration: BoxDecoration( + borderRadius: ROUNDED_BORDER_RADIUS, + border: Border.all( + color: color, + width: 1.0, + ), + ), + child: InkWell( + borderRadius: ROUNDED_BORDER_RADIUS, + onTap: onPressed, + child: IconTheme( + data: IconThemeData( + color: SmoothCardWithRoundedHeader.getHeaderColor(context), + ), + child: Row( + children: [ + SizedBox.square( + dimension: 34.0, + child: DecoratedBox( + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + child: Padding( + padding: padding ?? + const EdgeInsetsDirectional.all( + SMALL_SPACE, + ), + child: icon, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsetsDirectional.symmetric( + horizontal: MEDIUM_SPACE, + ), + child: Padding( + padding: const EdgeInsetsDirectional.only(bottom: 1.5), + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: color, + fontSize: 15.0, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} + +class _ExtractTextAnimation extends StatefulWidget { + const _ExtractTextAnimation({ + required this.tintColor, + required this.tintColorGradient, + }); + + final Color tintColor; + final Color tintColorGradient; + + @override + State<_ExtractTextAnimation> createState() => __ExtractTextAnimationState(); +} + +class __ExtractTextAnimationState extends State<_ExtractTextAnimation> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _progress; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1200), + ) + ..addListener(() => setState(() {})) + ..addStatusListener((AnimationStatus status) { + if (_controller.isCompleted) { + _controller.reverse(); + } + if (_controller.isDismissed) { + _controller.forward(); + } + }) + ..repeat(); + + _progress = Tween(begin: 0.0, end: 1.0) + .chain(CurveTween(curve: Curves.easeInOutCubic)) + .animate(_controller); + _controller.forward(); + } + + @override + Widget build(BuildContext context) { + final double progress = _progress.value; + + return CustomPaint( + painter: _ExtractTextAnimationPainter( + progress: progress, + lineColor: widget.tintColor.withValues( + alpha: progress.progressAndClamp(0.0, 0.8, 1.0), + ), + gradient: LinearGradient( + colors: [ + Colors.transparent, + widget.tintColorGradient.withValues( + alpha: progress, + ), + ], + stops: const [0.4, 1.0], + ), + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} + +class _ExtractTextAnimationPainter extends CustomPainter { + _ExtractTextAnimationPainter({ + required this.progress, + required this.gradient, + required this.lineColor, + }); + + final double progress; + final LinearGradient gradient; + final Color lineColor; + + @override + void paint(Canvas canvas, Size size) { + final double width = size.width * progress; + + final Paint paint = Paint() + ..shader = gradient.createShader( + Rect.fromLTWH(0.0, 0.0, width, size.height), + ); + + final Rect rect = Rect.fromLTWH(0.0, 0.0, width, size.height); + canvas.drawRect(rect, paint); + + paint + ..color = lineColor + ..strokeWidth = 3.0 + ..style = PaintingStyle.stroke + ..shader = null; + + canvas.drawLine( + Offset(width, 0), + Offset(width, size.height), + paint, + ); + } + + @override + bool shouldRepaint(_ExtractTextAnimationPainter oldDelegate) => + oldDelegate.progress != progress; + + @override + bool shouldRebuildSemantics(_ExtractTextAnimationPainter oldDelegate) => + false; +} diff --git a/packages/smooth_app/lib/pages/product/edit_ocr/edit_ocr_main_action.dart b/packages/smooth_app/lib/pages/product/edit_ocr/edit_ocr_main_action.dart deleted file mode 100644 index a6e797f07203..000000000000 --- a/packages/smooth_app/lib/pages/product/edit_ocr/edit_ocr_main_action.dart +++ /dev/null @@ -1,294 +0,0 @@ -part of 'edit_ocr_page.dart'; - -class _EditOcrMainAction extends StatelessWidget { - const _EditOcrMainAction({ - required this.onPressed, - required this.helper, - required this.state, - }); - - final VoidCallback onPressed; - final OcrHelper helper; - final _OcrState state; - - @override - Widget build(BuildContext context) { - final AppLocalizations appLocalizations = AppLocalizations.of(context); - - final Widget? child = switch (state) { - _OcrState.IMAGE_LOADING => _EditOcrActionLoadingContent( - helper: helper, - appLocalizations: appLocalizations, - ), - _OcrState.IMAGE_LOADED => _ExtractMainActionContentLoaded( - helper: helper, - appLocalizations: appLocalizations, - onPressed: onPressed, - ), - _OcrState.EXTRACTING_DATA => _EditOcrActionExtractingContent( - helper: helper, - appLocalizations: appLocalizations, - ), - _OcrState.OTHER => null, - }; - - if (child == null) { - return EMPTY_WIDGET; - } - - final SmoothColorsThemeExtension theme = - Theme.of(context).extension()!; - - return SizedBox( - height: 45.0 * (_computeFontScaleFactor(context)), - width: double.infinity, - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: ANGULAR_BORDER_RADIUS, - color: theme.primarySemiDark, - border: Border.all( - color: theme.primaryBlack, - width: 2.0, - ), - ), - child: Material( - type: MaterialType.transparency, - child: ProgressIndicatorTheme( - data: const ProgressIndicatorThemeData( - color: Colors.white, - ), - child: IconTheme( - data: const IconThemeData(color: Colors.white), - child: DefaultTextStyle( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 15.5, - color: Colors.white, - ), - child: child, - ), - ), - ), - ), - ), - ); - } - - double _computeFontScaleFactor(BuildContext context) { - final double fontSize = DefaultTextStyle.of(context).style.fontSize ?? 15.0; - final double scaledFontSize = - MediaQuery.textScalerOf(context).scale(fontSize); - - return scaledFontSize / fontSize; - } -} - -class _EditOcrActionExtractingContent extends StatelessWidget { - const _EditOcrActionExtractingContent({ - required this.helper, - required this.appLocalizations, - }); - - final OcrHelper helper; - final AppLocalizations appLocalizations; - - @override - Widget build(BuildContext context) { - return Semantics( - label: helper.getActionLoadingPhoto(appLocalizations), - excludeSemantics: true, - child: Shimmer( - gradient: const LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [ - Colors.black, - Colors.white, - Colors.black, - ], - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: MEDIUM_SPACE), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const _ExtractMainActionProgressIndicator(), - Expanded( - child: Text( - helper.getActionExtractingData(appLocalizations), - textAlign: TextAlign.center, - ), - ), - ], - ), - ), - ), - ); - } -} - -class _ExtractMainActionContentLoaded extends StatelessWidget { - const _ExtractMainActionContentLoaded({ - required this.helper, - required this.appLocalizations, - required this.onPressed, - }); - - final OcrHelper helper; - final AppLocalizations appLocalizations; - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - return Semantics( - excludeSemantics: true, - value: helper.getActionExtractText(appLocalizations), - button: true, - child: InkWell( - onTap: onPressed, - borderRadius: ANGULAR_BORDER_RADIUS, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: MEDIUM_SPACE), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Text( - helper.getActionExtractText(appLocalizations), - ), - ), - const Icon( - Icons.download, - semanticLabel: '', - ), - ], - ), - ), - ), - ); - } -} - -class _EditOcrActionLoadingContent extends StatelessWidget { - const _EditOcrActionLoadingContent({ - required this.helper, - required this.appLocalizations, - }); - - final OcrHelper helper; - final AppLocalizations appLocalizations; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsetsDirectional.only( - start: MEDIUM_SPACE, - end: VERY_SMALL_SPACE, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const _ExtractMainActionProgressIndicator(), - Expanded( - child: Text( - helper.getActionLoadingPhoto(appLocalizations), - ), - ), - AspectRatio( - aspectRatio: 1.0, - child: InkWell( - onTap: () => _openExplanation(context), - borderRadius: ANGULAR_BORDER_RADIUS, - child: Icon( - Icons.info_outline, - semanticLabel: helper.getActionLoadingPhotoDialogTitle( - appLocalizations, - ), - ), - ), - ) - ], - ), - ); - } - - void _openExplanation(BuildContext context) { - showDialog( - context: context, - builder: (BuildContext context) { - final AppLocalizations appLocalizations = AppLocalizations.of(context); - - return SmoothAlertDialog( - title: helper.getActionLoadingPhotoDialogTitle( - appLocalizations, - ), - leadingTitle: const Icon( - Icons.info_outline, - semanticLabel: '', - ), - close: true, - body: Text( - helper.getActionLoadingPhotoDialogBody( - appLocalizations, - ), - ), - positiveAction: SmoothActionButton( - text: appLocalizations.okay, - onPressed: () => Navigator.pop(context), - ), - ); - }, - ); - } -} - -/// We use a custom progress indicator, because Material and Cupertino Widgets -/// don't have the same size. -class _ExtractMainActionProgressIndicator extends StatelessWidget { - const _ExtractMainActionProgressIndicator(); - - @override - Widget build(BuildContext context) { - if (Platform.isIOS || Platform.isMacOS) { - return const Padding( - padding: EdgeInsetsDirectional.only( - start: SMALL_SPACE, - end: MEDIUM_SPACE, - top: SMALL_SPACE, - bottom: SMALL_SPACE, - ), - child: CupertinoActivityIndicator( - radius: BALANCED_SPACE, - color: Colors.white, - ), - ); - } - - return const Padding( - padding: EdgeInsetsDirectional.only( - start: SMALL_SPACE, - end: MEDIUM_SPACE, - top: MEDIUM_SPACE, - bottom: MEDIUM_SPACE, - ), - child: AspectRatio( - aspectRatio: 1.0, - child: CircularProgressIndicator( - strokeWidth: 2.5, - valueColor: AlwaysStoppedAnimation(Colors.white), - // backgroundColor: Colors.white, - ), - ), - ); - } -} - -enum _OcrState { - IMAGE_LOADING, - IMAGE_LOADED, - EXTRACTING_DATA, - OTHER, -} diff --git a/packages/smooth_app/lib/pages/product/edit_ocr/edit_ocr_page.dart b/packages/smooth_app/lib/pages/product/edit_ocr/edit_ocr_page.dart index f228cdd49d50..90b2cdfff11c 100644 --- a/packages/smooth_app/lib/pages/product/edit_ocr/edit_ocr_page.dart +++ b/packages/smooth_app/lib/pages/product/edit_ocr/edit_ocr_page.dart @@ -1,38 +1,34 @@ import 'dart:io'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:provider/provider.dart'; -import 'package:shimmer/shimmer.dart'; +import 'package:provider/single_child_widget.dart'; import 'package:smooth_app/background/background_task_details.dart'; import 'package:smooth_app/data_models/preferences/user_preferences.dart'; import 'package:smooth_app/data_models/up_to_date_mixin.dart'; import 'package:smooth_app/database/local_database.dart'; import 'package:smooth_app/database/transient_file.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/picture_not_found.dart'; import 'package:smooth_app/helpers/analytics_helper.dart'; import 'package:smooth_app/helpers/product_cards_helper.dart'; -import 'package:smooth_app/helpers/provider_helper.dart'; -import 'package:smooth_app/pages/image_crop_page.dart'; -import 'package:smooth_app/pages/preferences/user_preferences_dev_mode.dart'; +import 'package:smooth_app/helpers/ui_helpers.dart'; import 'package:smooth_app/pages/product/common/product_refresher.dart'; +import 'package:smooth_app/pages/product/edit_ocr/edit_ocr_image.dart'; import 'package:smooth_app/pages/product/edit_ocr/edit_ocr_tabbar.dart'; +import 'package:smooth_app/pages/product/edit_ocr/edit_ocr_textfield.dart'; import 'package:smooth_app/pages/product/edit_ocr/ocr_helper.dart'; -import 'package:smooth_app/pages/product/explanation_widget.dart'; +import 'package:smooth_app/pages/product/gallery_view/product_image_gallery_view.dart'; import 'package:smooth_app/pages/product/multilingual_helper.dart'; -import 'package:smooth_app/pages/product/product_image_button.dart'; +import 'package:smooth_app/pages/product/product_image_swipeable_view.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_scaffold.dart'; import 'package:smooth_app/widgets/v2/smooth_buttons_bar.dart'; -part 'edit_ocr_main_action.dart'; - /// Editing with OCR a product field and the corresponding image. /// /// Typical use-cases: ingredients and packaging. @@ -72,6 +68,114 @@ class _EditOcrPageState extends State with UpToDateMixin { ); } + @override + Widget build(BuildContext context) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + context.watch(); + refreshUpToDate(); + final TransientFile transientFile = TransientFile.fromProduct( + upToDateProduct, + _helper.getImageField(), + _multilingualHelper.getCurrentLanguage(), + ); + + // TODO(monsieurtanuki): add WillPopScope / MayExitPage system + return MultiProvider( + providers: [ + Provider( + create: (BuildContext context) => upToDateProduct, + ), + Provider.value( + value: _extractState(transientFile), + ), + ], + child: SmoothScaffold( + extendBodyBehindAppBar: false, + appBar: buildEditProductAppBar( + context: context, + title: _helper.getTitle(appLocalizations), + product: upToDateProduct, + bottom: !_multilingualHelper.isMonolingual() + ? EditOcrTabBar( + onTabChanged: (OpenFoodFactsLanguage language) { + if (_multilingualHelper.changeLanguage(language)) { + onNextFrame( + () => setState(() {}), + forceRedraw: true, + ); + } + }, + imageField: _helper.getImageField(), + languagesWithText: _getLanguagesWithText(), + ) + : null, + ), + backgroundColor: context.lightTheme() + ? context.extension().primaryLight + : null, + body: ListView( + padding: const EdgeInsetsDirectional.all(MEDIUM_SPACE), + children: [ + EditOCRImageWidget( + helper: _helper, + transientFile: transientFile, + ownerField: upToDateProduct.isImageLocked( + _helper.getImageField(), + _multilingualHelper.getCurrentLanguage(), + ) ?? + false, + onEditImage: () async => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ProductImageSwipeableView.imageField( + imageField: _helper.getImageField(), + product: upToDateProduct, + isLoggedInMandatory: widget.isLoggedInMandatory, + ), + ), + ), + onExtractText: () async => _extractData(), + onTakePicture: () async => _takePicture(), + ), + const SizedBox(height: MEDIUM_SPACE), + EditOCRTextField( + helper: _helper, + controller: _controller, + extraButton: _helper.hasAddExtraPhotoButton() + ? EditOCRExtraButton( + barcode: upToDateProduct.barcode!, + productType: upToDateProduct.productType, + multilingualHelper: _multilingualHelper, + isLoggedInMandatory: widget.isLoggedInMandatory, + ) + : null, + isOwnerField: _helper.isOwnerField( + upToDateProduct, + _multilingualHelper.getCurrentLanguage(), + ), + ), + ], + ), + bottomNavigationBar: SmoothButtonsBar2( + positiveButton: SmoothActionButton2( + text: appLocalizations.save, + onPressed: () async { + await _updateText(); + if (!context.mounted) { + return; + } + Navigator.pop(context); + }, + ), + negativeButton: SmoothActionButton2( + text: appLocalizations.cancel, + onPressed: () => Navigator.pop(context), + ), + ), + ), + ); + } + /// Extracts data with OCR from the image stored on the server. /// /// When done, populates the related page field. @@ -104,6 +208,16 @@ class _EditOcrPageState extends State with UpToDateMixin { } } + Future _takePicture() async { + return ProductImageGalleryView.takePicture( + context: context, + product: upToDateProduct, + language: _multilingualHelper.getCurrentLanguage(), + imageField: _helper.getImageField(), + pictureSource: UserPictureSource.SELECT, + ); + } + /// Updates the product field on the server. Future _updateText() async { final Product? changedProduct = _getMinimalistProduct(); @@ -133,48 +247,6 @@ class _EditOcrPageState extends State with UpToDateMixin { return; } - @override - Widget build(BuildContext context) { - final AppLocalizations appLocalizations = AppLocalizations.of(context); - context.watch(); - refreshUpToDate(); - final TransientFile transientFile = TransientFile.fromProduct( - upToDateProduct, - _helper.getImageField(), - _multilingualHelper.getCurrentLanguage(), - ); - - // TODO(monsieurtanuki): add WillPopScope / MayExitPage system - return Provider( - create: (BuildContext context) => upToDateProduct, - child: SmoothScaffold( - extendBodyBehindAppBar: true, - appBar: buildEditProductAppBar( - context: context, - title: _helper.getTitle(appLocalizations), - product: upToDateProduct, - bottom: !_multilingualHelper.isMonolingual() - ? EditOcrTabBar( - onTabChanged: (OpenFoodFactsLanguage language) { - if (_multilingualHelper.changeLanguage(language)) { - setState(() {}); - } - }, - imageField: _helper.getImageField(), - languagesWithText: _getLanguagesWithText(), - ) - : null, - ), - body: Stack( - children: [ - _getImageWidget(transientFile), - _getOcrWidget(transientFile), - ], - ), - ), - ); - } - List _getLanguagesWithText() { final Map allLanguages = _multilingualHelper.getInitialMultiLingualTexts(); @@ -190,260 +262,6 @@ class _EditOcrPageState extends State with UpToDateMixin { return languages; } - Widget _getImageButton( - final ProductImageButtonType type, - final bool imageExists, - ) => - Padding( - padding: const EdgeInsets.symmetric(horizontal: SMALL_SPACE), - child: type.getButton( - product: upToDateProduct, - imageField: _helper.getImageField(), - imageExists: imageExists, - language: _multilingualHelper.getCurrentLanguage(), - isLoggedInMandatory: widget.isLoggedInMandatory, - borderWidth: 2, - ), - ); - - Widget _getImageWidget(final TransientFile transientFile) { - final Size size = MediaQuery.sizeOf(context); - final AppLocalizations appLocalizations = AppLocalizations.of(context); - final ImageProvider? imageProvider = transientFile.getImageProvider(); - - if (imageProvider != null) { - return ConstrainedBox( - constraints: const BoxConstraints.expand(), - child: InteractiveViewer( - boundaryMargin: const EdgeInsets.only( - left: VERY_LARGE_SPACE, - top: 10, - right: VERY_LARGE_SPACE, - bottom: 200, - ), - minScale: 0.1, - maxScale: 5, - child: Image( - fit: BoxFit.contain, - image: imageProvider, - ), - ), - ); - } - - return Container( - alignment: Alignment.center, - margin: EdgeInsets.only(bottom: size.height * 0.25), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - width: size.width, - height: size.height / 4, - child: const PictureNotFound(boxFit: BoxFit.fitHeight), - ), - Padding( - padding: const EdgeInsets.all(LARGE_SPACE), - child: Text( - appLocalizations.ocr_image_upload_instruction, - style: Theme.of(context).textTheme.bodyMedium, - textAlign: TextAlign.center, - ), - ) - ], - ), - ); - } - - Widget _getOcrWidget(final TransientFile transientFile) { - final AppLocalizations appLocalizations = AppLocalizations.of(context); - final OpenFoodFactsLanguage language = - _multilingualHelper.getCurrentLanguage(); - final ImageProvider? imageProvider = transientFile.getImageProvider(); - final bool imageExists = imageProvider != null; - - return Align( - alignment: AlignmentDirectional.bottomStart, - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Flexible( - flex: 1, - child: Padding( - padding: const EdgeInsetsDirectional.only( - bottom: LARGE_SPACE, - start: LARGE_SPACE, - end: LARGE_SPACE, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: _getImageButton( - ProductImageButtonType.server, - imageExists, - ), - ), - Expanded( - child: _getImageButton( - ProductImageButtonType.local, - imageExists, - ), - ), - ], - ), - if (imageProvider != null) - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: _getImageButton( - ProductImageButtonType.unselect, - imageExists, - ), - ), - Expanded( - child: _getImageButton( - ProductImageButtonType.edit, - imageExists, - ), - ), - ], - ), - ], - ), - ), - ), - Flexible( - flex: 1, - child: DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: const BorderRadiusDirectional.only( - topStart: ANGULAR_RADIUS, - topEnd: ANGULAR_RADIUS, - ), - ), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsetsDirectional.only( - start: LARGE_SPACE, - end: LARGE_SPACE, - top: LARGE_SPACE, - ), - child: Column( - children: [ - _EditOcrMainAction( - onPressed: _extractData, - helper: _helper, - state: _extractState(transientFile), - ), - const SizedBox(height: MEDIUM_SPACE), - ConsumerFilter( - buildWhen: ( - UserPreferences? previousValue, - UserPreferences currentValue, - ) { - return previousValue?.getFlag(UserPreferencesDevMode - .userPreferencesFlagSpellCheckerOnOcr) != - currentValue.getFlag(UserPreferencesDevMode - .userPreferencesFlagSpellCheckerOnOcr); - }, - builder: ( - BuildContext context, - UserPreferences prefs, - Widget? child, - ) { - final ThemeData theme = Theme.of(context); - - return Theme( - data: theme.copyWith( - colorScheme: theme.colorScheme.copyWith( - onSurface: context - .read() - .isDarkMode(context) - ? Colors.white - : Colors.black, - ), - ), - child: TextField( - controller: _controller, - decoration: InputDecoration( - fillColor: Colors.white.withValues(alpha: 0.2), - filled: true, - enabledBorder: const OutlineInputBorder( - borderRadius: ANGULAR_BORDER_RADIUS, - ), - ), - maxLines: null, - textInputAction: TextInputAction.newline, - spellCheckConfiguration: (prefs.getFlag( - UserPreferencesDevMode - .userPreferencesFlagSpellCheckerOnOcr) ?? - false) && - (Platform.isAndroid || Platform.isIOS) - ? const SpellCheckConfiguration() - : const SpellCheckConfiguration.disabled(), - ), - ); - }, - ), - const SizedBox(height: SMALL_SPACE), - ExplanationWidget( - _helper.getInstructions(appLocalizations), - ), - if (_helper.hasAddExtraPhotoButton()) - Padding( - padding: const EdgeInsets.only(top: SMALL_SPACE), - child: addPanelButton( - appLocalizations.add_packaging_photo_button_label - .toUpperCase(), - onPressed: () async => confirmAndUploadNewPicture( - context, - imageField: ImageField.OTHER, - barcode: barcode, - productType: upToDateProduct.productType, - language: language, - isLoggedInMandatory: widget.isLoggedInMandatory, - ), - iconData: Icons.add_a_photo, - ), - ), - ], - ), - ), - ), - ), - ), - SmoothButtonsBar2( - positiveButton: SmoothActionButton2( - text: appLocalizations.save, - onPressed: () async { - await _updateText(); - if (!mounted) { - return; - } - Navigator.pop(context); - }, - ), - negativeButton: SmoothActionButton2( - text: appLocalizations.cancel, - onPressed: () => Navigator.pop(context), - ), - ), - ], - ), - ); - } - /// Returns a [Product] with the values from the text fields. Product? _getMinimalistProduct() { Product? result; @@ -467,15 +285,22 @@ class _EditOcrPageState extends State with UpToDateMixin { return result; } - _OcrState _extractState(TransientFile transientFile) { + OcrState _extractState(TransientFile transientFile) { if (_extractingData) { - return _OcrState.EXTRACTING_DATA; + return OcrState.EXTRACTING_DATA; } else if (transientFile.isServerImage()) { - return _OcrState.IMAGE_LOADED; + return OcrState.IMAGE_LOADED; } else if (transientFile.isImageAvailable()) { - return _OcrState.IMAGE_LOADING; + return OcrState.IMAGE_LOADING; } else { - return _OcrState.OTHER; + return OcrState.OTHER; } } } + +enum OcrState { + IMAGE_LOADING, + IMAGE_LOADED, + EXTRACTING_DATA, + OTHER, +} diff --git a/packages/smooth_app/lib/pages/product/edit_ocr/edit_ocr_textfield.dart b/packages/smooth_app/lib/pages/product/edit_ocr/edit_ocr_textfield.dart new file mode 100644 index 000000000000..a69efc021d9c --- /dev/null +++ b/packages/smooth_app/lib/pages/product/edit_ocr/edit_ocr_textfield.dart @@ -0,0 +1,201 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; +import 'package:provider/provider.dart'; +import 'package:smooth_app/data_models/preferences/user_preferences.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/product_cards_helper.dart'; +import 'package:smooth_app/helpers/provider_helper.dart'; +import 'package:smooth_app/pages/image_crop_page.dart'; +import 'package:smooth_app/pages/preferences/user_preferences_dev_mode.dart'; +import 'package:smooth_app/pages/product/edit_ocr/edit_ocr_page.dart'; +import 'package:smooth_app/pages/product/edit_ocr/ocr_helper.dart'; +import 'package:smooth_app/pages/product/multilingual_helper.dart'; +import 'package:smooth_app/pages/product/owner_field_info.dart'; +import 'package:smooth_app/pages/product/simple_input_widget.dart'; +import 'package:smooth_app/themes/theme_provider.dart'; + +class EditOCRTextField extends StatelessWidget { + const EditOCRTextField({ + required this.helper, + required this.controller, + required this.isOwnerField, + this.extraButton, + }); + + final OcrHelper helper; + final TextEditingController controller; + final bool isOwnerField; + final Widget? extraButton; + + @override + Widget build(BuildContext context) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + + return Consumer( + builder: ( + BuildContext context, + OcrState ocrState, + Widget? child, + ) { + if (ocrState == OcrState.EXTRACTING_DATA) { + return AbsorbPointer( + absorbing: ocrState == OcrState.EXTRACTING_DATA, + child: Opacity( + opacity: 0.2, + child: child, + ), + ); + } else { + return child!; + } + }, + child: SmoothCardWithRoundedHeader( + title: helper.getEditableContentTitle(appLocalizations), + leading: helper.getIcon().call(context), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (isOwnerField) const EditOCROwnerFieldIcon(), + ExplanationTitleIcon( + type: helper.getType(appLocalizations), + text: helper.getInstructions(appLocalizations), + ), + ], + ), + titlePadding: const EdgeInsetsDirectional.only( + top: 2.0, + start: LARGE_SPACE, + end: SMALL_SPACE, + bottom: 2.0, + ), + contentPadding: const EdgeInsetsDirectional.all( + MEDIUM_SPACE, + ), + child: Column( + children: [ + ConsumerFilter( + buildWhen: ( + UserPreferences? previousValue, + UserPreferences currentValue, + ) { + return previousValue?.getFlag(UserPreferencesDevMode + .userPreferencesFlagSpellCheckerOnOcr) != + currentValue.getFlag(UserPreferencesDevMode + .userPreferencesFlagSpellCheckerOnOcr); + }, + builder: ( + BuildContext context, + UserPreferences prefs, + Widget? child, + ) { + final ThemeData theme = Theme.of(context); + + return Theme( + data: theme.copyWith( + colorScheme: theme.colorScheme.copyWith( + onSurface: + context.read().isDarkMode(context) + ? Colors.white + : Colors.black, + ), + ), + child: TextFormField( + minLines: null, + maxLines: null, + controller: controller, + textInputAction: TextInputAction.newline, + spellCheckConfiguration: (prefs.getFlag( + UserPreferencesDevMode + .userPreferencesFlagSpellCheckerOnOcr) ?? + false) && + (Platform.isAndroid || Platform.isIOS) + ? const SpellCheckConfiguration() + : const SpellCheckConfiguration.disabled(), + decoration: const InputDecoration( + contentPadding: EdgeInsets.symmetric( + horizontal: LARGE_SPACE, + vertical: SMALL_SPACE, + ), + filled: true, + border: OutlineInputBorder( + borderRadius: ROUNDED_BORDER_RADIUS, + ), + enabledBorder: OutlineInputBorder( + borderRadius: ROUNDED_BORDER_RADIUS, + borderSide: BorderSide( + color: Colors.transparent, + width: 5.0, + ), + ), + ), + ), + ); + }, + ), + if (extraButton != null) extraButton!, + ], + ), + ), + ); + } +} + +class EditOCRExtraButton extends StatelessWidget { + const EditOCRExtraButton({ + required this.barcode, + required this.productType, + required this.multilingualHelper, + required this.isLoggedInMandatory, + }); + + final String barcode; + final ProductType? productType; + final MultilingualHelper multilingualHelper; + final bool isLoggedInMandatory; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: SMALL_SPACE), + child: addPanelButton( + AppLocalizations.of(context).add_packaging_photo_button_label, + onPressed: () async => confirmAndUploadNewPicture( + context, + imageField: ImageField.OTHER, + barcode: barcode, + productType: productType, + language: multilingualHelper.getCurrentLanguage(), + isLoggedInMandatory: isLoggedInMandatory, + ), + iconData: Icons.add_a_photo_rounded, + padding: const EdgeInsetsDirectional.only( + top: SMALL_SPACE, + bottom: SMALL_SPACE, + start: VERY_SMALL_SPACE, + ), + ), + ); + } +} + +class EditOCROwnerFieldIcon extends StatelessWidget { + const EditOCROwnerFieldIcon(); + + @override + Widget build(BuildContext context) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + + return SmoothCardHeaderButton( + tooltip: appLocalizations.owner_field_info_title, + child: const OwnerFieldIcon(), + onTap: () => showOwnerFieldInfoInModalSheet( + context, + headerColor: SmoothCardWithRoundedHeader.getHeaderColor(context), + ), + ); + } +} diff --git a/packages/smooth_app/lib/pages/product/edit_ocr/ocr_helper.dart b/packages/smooth_app/lib/pages/product/edit_ocr/ocr_helper.dart index f8cdc759b715..49baac1df7f3 100644 --- a/packages/smooth_app/lib/pages/product/edit_ocr/ocr_helper.dart +++ b/packages/smooth_app/lib/pages/product/edit_ocr/ocr_helper.dart @@ -39,6 +39,9 @@ abstract class OcrHelper { /// Returns the "extract text" button label String getActionExtractText(final AppLocalizations appLocalizations); + /// Returns the "extract text" button label (short version) + String getActionExtractShortText(final AppLocalizations appLocalizations); + /// Returns the "loading photo" button label String getActionLoadingPhoto(final AppLocalizations appLocalizations); @@ -68,6 +71,25 @@ abstract class OcrHelper { /// Returns the image field we try to run OCR on. ImageField getImageField(); + /// Returns the type of the field (eg: ingredients). + String getType(final AppLocalizations appLocalizations); + + /// Eg: list of ingredients, packaging, etc. + String getEditableContentTitle(final AppLocalizations appLocalizations); + + /// Eg: Ingredients photo, packaging photo, etc. + String getPhotoTitle(final AppLocalizations appLocalizations); + + /// Returns the icon to be displayed in the edit page. + WidgetBuilder getIcon(); + + /// Returns if a given product has an owner field + /// (= value provided by the producer). + bool isOwnerField( + final Product product, + final OpenFoodFactsLanguage language, + ); + /// Returns the text that the server OCR managed to extract from the image. Future getExtractedText( final Product product, diff --git a/packages/smooth_app/lib/pages/product/edit_ocr/ocr_ingredients_helper.dart b/packages/smooth_app/lib/pages/product/edit_ocr/ocr_ingredients_helper.dart index dbc5173ec953..d268fe0bc920 100644 --- a/packages/smooth_app/lib/pages/product/edit_ocr/ocr_ingredients_helper.dart +++ b/packages/smooth_app/lib/pages/product/edit_ocr/ocr_ingredients_helper.dart @@ -1,9 +1,11 @@ +import 'package:flutter/widgets.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:smooth_app/background/background_task_details.dart'; import 'package:smooth_app/helpers/analytics_helper.dart'; import 'package:smooth_app/pages/product/edit_ocr/ocr_helper.dart'; import 'package:smooth_app/query/product_query.dart'; +import 'package:smooth_app/resources/app_icons.dart' as icons; /// OCR Helper for ingredients. class OcrIngredientsHelper extends OcrHelper { @@ -45,6 +47,10 @@ class OcrIngredientsHelper extends OcrHelper { String getActionExtractText(final AppLocalizations appLocalizations) => appLocalizations.edit_ingredients_extract_ingredients_btn_text; + @override + String getActionExtractShortText(final AppLocalizations appLocalizations) => + appLocalizations.edit_ingredients_extract_ingredients_btn_text_short; + @override String getActionExtractingData(AppLocalizations appLocalizations) => appLocalizations.edit_ingredients_extracting_ingredients_btn_text; @@ -73,6 +79,18 @@ class OcrIngredientsHelper extends OcrHelper { String getTitle(final AppLocalizations appLocalizations) => appLocalizations.ingredients_editing_title; + @override + String getType(final AppLocalizations appLocalizations) => + appLocalizations.ingredients; + + @override + String getEditableContentTitle(final AppLocalizations appLocalizations) => + appLocalizations.edit_product_ingredients_list_title; + + @override + String getPhotoTitle(final AppLocalizations appLocalizations) => + appLocalizations.edit_product_ingredients_photo_title; + @override String getAddButtonLabel(final AppLocalizations appLocalizations) => appLocalizations.score_add_missing_ingredients; @@ -80,6 +98,14 @@ class OcrIngredientsHelper extends OcrHelper { @override ImageField getImageField() => ImageField.INGREDIENTS; + @override + bool isOwnerField( + final Product product, + final OpenFoodFactsLanguage language, + ) => + product.ownerFields?.containsKey('ingredients_text_${language.offTag}') ?? + false; + @override Future getExtractedText( final Product product, @@ -107,4 +133,9 @@ class OcrIngredientsHelper extends OcrHelper { @override AnalyticsEditEvents getEditEventAnalyticsTag() => AnalyticsEditEvents.ingredients_and_Origins; + + @override + WidgetBuilder getIcon() { + return (BuildContext context) => const icons.Ingredients(); + } } diff --git a/packages/smooth_app/lib/pages/product/edit_ocr/ocr_packaging_helper.dart b/packages/smooth_app/lib/pages/product/edit_ocr/ocr_packaging_helper.dart index 78795345465c..6a22424fbf3d 100644 --- a/packages/smooth_app/lib/pages/product/edit_ocr/ocr_packaging_helper.dart +++ b/packages/smooth_app/lib/pages/product/edit_ocr/ocr_packaging_helper.dart @@ -1,11 +1,13 @@ import 'dart:async'; +import 'package:flutter/widgets.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:smooth_app/background/background_task_details.dart'; import 'package:smooth_app/helpers/analytics_helper.dart'; import 'package:smooth_app/pages/product/edit_ocr/ocr_helper.dart'; import 'package:smooth_app/query/product_query.dart'; +import 'package:smooth_app/resources/app_icons.dart' as icons; /// OCR Helper for packaging. class OcrPackagingHelper extends OcrHelper { @@ -48,6 +50,10 @@ class OcrPackagingHelper extends OcrHelper { String getActionExtractText(final AppLocalizations appLocalizations) => appLocalizations.edit_packaging_extract_btn_text; + @override + String getActionExtractShortText(final AppLocalizations appLocalizations) => + appLocalizations.edit_packaging_extract_btn_text_short; + @override String getActionExtractingData(AppLocalizations appLocalizations) => appLocalizations.edit_packaging_extracting_btn_text; @@ -76,10 +82,26 @@ class OcrPackagingHelper extends OcrHelper { String getTitle(final AppLocalizations appLocalizations) => appLocalizations.packaging_editing_title; + @override + String getEditableContentTitle(final AppLocalizations appLocalizations) => + appLocalizations.edit_product_packaging_list_title; + + @override + String getPhotoTitle(final AppLocalizations appLocalizations) => + appLocalizations.edit_product_packaging_photo_title; + + @override + String getType(final AppLocalizations appLocalizations) => + appLocalizations.edit_packagings_title; + @override String getAddButtonLabel(final AppLocalizations appLocalizations) => appLocalizations.score_add_missing_packaging_image; + /// Not supported yet + @override + bool isOwnerField(Product product, OpenFoodFactsLanguage language) => false; + @override ImageField getImageField() => ImageField.PACKAGING; @@ -109,4 +131,9 @@ class OcrPackagingHelper extends OcrHelper { @override AnalyticsEditEvents getEditEventAnalyticsTag() => AnalyticsEditEvents.recyclingInstructionsPhotos; + + @override + WidgetBuilder getIcon() { + return (BuildContext context) => const icons.Packaging(); + } } diff --git a/packages/smooth_app/lib/pages/product/owner_field_info.dart b/packages/smooth_app/lib/pages/product/owner_field_info.dart index ac9c9076f4ee..5918a18fcbfa 100644 --- a/packages/smooth_app/lib/pages/product/owner_field_info.dart +++ b/packages/smooth_app/lib/pages/product/owner_field_info.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:smooth_app/generic_lib/bottom_sheets/smooth_bottom_sheet.dart'; import 'package:smooth_app/generic_lib/duration_constants.dart'; import 'package:smooth_app/widgets/smooth_banner.dart'; import 'package:smooth_app/widgets/widget_height.dart'; @@ -167,3 +168,24 @@ class _AnimatedOwnerFieldBannerState extends State super.dispose(); } } + +Future showOwnerFieldInfoInModalSheet( + BuildContext context, { + Color? headerColor, +}) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + + return showSmoothModalSheet( + context: context, + builder: (BuildContext context) { + return SmoothModalSheet( + title: appLocalizations.owner_field_info_title, + headerBackgroundColor: headerColor, + prefixIndicator: true, + body: SmoothModalSheetBodyContainer( + child: Text(appLocalizations.owner_field_info_message), + ), + ); + }, + ); +} diff --git a/packages/smooth_app/lib/pages/product/product_image_viewer.dart b/packages/smooth_app/lib/pages/product/product_image_viewer.dart index 1b7d04d5d94e..ab84f17c6021 100644 --- a/packages/smooth_app/lib/pages/product/product_image_viewer.dart +++ b/packages/smooth_app/lib/pages/product/product_image_viewer.dart @@ -100,19 +100,36 @@ class _ProductImageViewerState extends State child: imageProvider == null ? Stack( children: [ - const SizedBox.expand(child: PictureNotFound()), - Center( - child: Text( - selectedLanguages.isEmpty - ? appLocalizations.edit_photo_language_none - : appLocalizations - .edit_photo_language_not_this_one, - style: Theme.of(context) - .textTheme - .headlineMedium - ?.copyWith(color: Colors.black) ?? - const TextStyle(color: Colors.black), - textAlign: TextAlign.center, + const Positioned.fill( + child: ExcludeSemantics( + child: PictureNotFound(), + ), + ), + Align( + alignment: const Alignment(0.0, -0.8), + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: ROUNDED_BORDER_RADIUS, + ), + child: Padding( + padding: const EdgeInsetsDirectional.symmetric( + horizontal: LARGE_SPACE, + vertical: SMALL_SPACE, + ), + child: Text( + selectedLanguages.isEmpty + ? appLocalizations.edit_photo_language_none + : appLocalizations + .edit_photo_language_not_this_one, + style: Theme.of(context) + .textTheme + .headlineMedium + ?.copyWith(color: Colors.black) ?? + const TextStyle(color: Colors.black), + textAlign: TextAlign.center, + ), + ), ), ), Positioned.fill( diff --git a/packages/smooth_app/lib/pages/product/simple_input_widget.dart b/packages/smooth_app/lib/pages/product/simple_input_widget.dart index 22a0d2cdb25a..9cd38bb3e465 100644 --- a/packages/smooth_app/lib/pages/product/simple_input_widget.dart +++ b/packages/smooth_app/lib/pages/product/simple_input_widget.dart @@ -189,17 +189,17 @@ class _SimpleInputWidgetState extends State { leading: widget.helper.getIcon(), title: widget.helper.getTitle(appLocalizations), trailing: explanations != null && widget.displayTitle - ? _ExplanationTitleIcon( + ? ExplanationTitleIcon( text: explanations, type: widget.helper.getTitle(appLocalizations), ) : null, - titlePadding: explanations != null + titlePadding: explanations != null && widget.displayTitle ? const EdgeInsetsDirectional.only( - top: SMALL_SPACE, - bottom: SMALL_SPACE, + top: 2.0, start: LARGE_SPACE, end: SMALL_SPACE, + bottom: 2.0, ) : null, child: child, @@ -253,8 +253,8 @@ class _SimpleInputWidgetState extends State { } } -class _ExplanationTitleIcon extends StatelessWidget { - const _ExplanationTitleIcon({ +class ExplanationTitleIcon extends StatelessWidget { + const ExplanationTitleIcon({ required this.type, required this.text, }); @@ -267,50 +267,26 @@ class _ExplanationTitleIcon extends StatelessWidget { final String title = AppLocalizations.of(context).edit_product_form_item_help(type); - return Material( - type: MaterialType.transparency, - child: Semantics( - label: title, - button: true, - excludeSemantics: true, - child: Tooltip( - message: title, - child: InkWell( - customBorder: const CircleBorder(), - onTap: () { - showSmoothModalSheet( - context: context, - builder: (BuildContext context) { - return SmoothModalSheet( - title: title, - prefixIndicator: true, - body: Padding( - padding: EdgeInsetsDirectional.only( - start: MEDIUM_SPACE, - end: MEDIUM_SPACE, - top: VERY_SMALL_SPACE, - bottom: VERY_SMALL_SPACE + - MediaQuery.viewPaddingOf(context).bottom, - ), - child: Text( - text, - style: const TextStyle( - fontSize: 15.0, - height: 1.7, - ), - ), - ), - ); - }, - ); - }, - child: const Padding( - padding: EdgeInsetsDirectional.all(MEDIUM_SPACE), - child: icons.Help(), - ), - ), - ), - ), + return SmoothCardHeaderButton( + tooltip: title, + child: const icons.Help(), + onTap: () { + showSmoothModalSheet( + context: context, + builder: (BuildContext context) { + return SmoothModalSheet( + title: title, + prefixIndicator: true, + headerBackgroundColor: SmoothCardWithRoundedHeader.getHeaderColor( + context, + ), + body: SmoothModalSheetBodyContainer( + child: Text(text), + ), + ); + }, + ); + }, ); } } diff --git a/packages/smooth_app/lib/pages/scan/carousel/main_card/scan_main_card.dart b/packages/smooth_app/lib/pages/scan/carousel/main_card/scan_main_card.dart index f8c4e29d3c9f..ba83efdecb71 100644 --- a/packages/smooth_app/lib/pages/scan/carousel/main_card/scan_main_card.dart +++ b/packages/smooth_app/lib/pages/scan/carousel/main_card/scan_main_card.dart @@ -138,7 +138,7 @@ class _SearchCard extends StatelessWidget { TextWithBoldParts( text: localizations.homepage_main_card_subheading, textAlign: TextAlign.center, - textStyle: const TextStyle(height: 1.3, fontSize: 15.0), + textStyle: const TextStyle(height: 1.6, fontSize: 15.0), ), const SizedBox(height: MEDIUM_SPACE), const Padding( diff --git a/packages/smooth_app/lib/resources/app_icons.dart b/packages/smooth_app/lib/resources/app_icons.dart index f7c6a53a0ec7..9b8dce05af1d 100644 --- a/packages/smooth_app/lib/resources/app_icons.dart +++ b/packages/smooth_app/lib/resources/app_icons.dart @@ -1001,7 +1001,7 @@ class Milk extends AppIcon { super.key, }) : super._(_IconsFont.milk); - const Milk.filled({ + const Milk.happy({ super.color, super.size, super.shadow, @@ -1009,6 +1009,14 @@ class Milk extends AppIcon { super.key, }) : super._(_IconsFont.milk_filled); + const Milk.unhappy({ + super.color, + super.size, + super.shadow, + super.semanticLabel, + super.key, + }) : super._(_IconsFont.milk_filled_unhappy); + const Milk.download({ super.color, super.size, @@ -1018,6 +1026,16 @@ class Milk extends AppIcon { }) : super._(_IconsFont.milk_download); } +class Move extends AppIcon { + const Move({ + super.color, + super.size, + super.shadow, + super.semanticLabel, + super.key, + }) : super._(_IconsFont.move); +} + class NoPicture extends AppIcon { const NoPicture({ super.color, @@ -1046,6 +1064,16 @@ class NutritionFacts extends AppIcon { }) : super._(_IconsFont.nutritional_facts); } +class OCR extends AppIcon { + const OCR({ + super.color, + super.size, + super.shadow, + super.semanticLabel, + super.key, + }) : super._(_IconsFont.ocr); +} + class Outdated extends AppIcon { const Outdated({ super.color, diff --git a/packages/smooth_app/lib/resources/app_icons_font.dart b/packages/smooth_app/lib/resources/app_icons_font.dart index ca2669d03061..d21212bb9cf4 100644 --- a/packages/smooth_app/lib/resources/app_icons_font.dart +++ b/packages/smooth_app/lib/resources/app_icons_font.dart @@ -221,6 +221,8 @@ class _IconsFont { IconData(0xe868, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData add_to_list_6 = IconData(0xe869, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData ocr = + IconData(0xe86a, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData add_to_list_7 = IconData(0xe86b, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData add_to_list_8 = @@ -251,6 +253,10 @@ class _IconsFont { IconData(0xe87c, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData gallery = IconData(0xe87d, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData milk_filled_unhappy = + IconData(0xe87e, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData move = + IconData(0xe87f, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData share_cupertino = IconData(0xe8a4, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData share_material = diff --git a/packages/smooth_app/lib/widgets/smooth_indicator_icon.dart b/packages/smooth_app/lib/widgets/smooth_indicator_icon.dart new file mode 100644 index 000000000000..74a4e7b755f8 --- /dev/null +++ b/packages/smooth_app/lib/widgets/smooth_indicator_icon.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:smooth_app/generic_lib/design_constants.dart'; + +/// A circle icon (generally used to show an action) +class SmoothIndicatorIcon extends StatelessWidget { + const SmoothIndicatorIcon({ + required this.icon, + this.iconTheme, + this.margin, + this.padding, + super.key, + }); + + final Widget icon; + final IconThemeData? iconTheme; + final EdgeInsetsGeometry? margin; + final EdgeInsetsGeometry? padding; + + @override + Widget build(BuildContext context) { + return Padding( + padding: margin ?? + const EdgeInsetsDirectional.all( + VERY_SMALL_SPACE, + ), + child: DecoratedBox( + decoration: const BoxDecoration( + color: Colors.black38, + shape: BoxShape.circle, + ), + child: Padding( + padding: padding ?? const EdgeInsetsDirectional.all(SMALL_SPACE), + child: IconTheme( + data: iconTheme ?? + const IconThemeData( + color: Colors.white, + size: 15.0, + ), + child: icon, + ), + ), + ), + ); + } +}