diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index 884cc740c29..1dca9904e91 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -1448,18 +1448,34 @@ "completed_basic_details_btn_text": "Complete basic details", "not_implemented_snackbar_text": "Not implemented yet", "category_picker_page_appbar_text": "Categories", - "edit_ingredients_extrait_ingredients_btn_text": "Extract ingredients", - "@edit_ingredients_extrait_ingredients_btn_text": { + "edit_ingredients_extract_ingredients_btn_text": "Extract ingredients from the photo", + "@edit_ingredients_extract_ingredients_btn_text": { "description": "Ingredients edition - Extract ingredients" }, + "edit_ingredients_extracting_ingredients_btn_text": "Extracting ingredients\nfrom the photo", + "@edit_ingredients_extracting_ingredients_btn_text": { + "description": "Ingredients edition - Extracting ingredients" + }, + "edit_ingredients_loading_photo_btn_text": "Loading photo…", + "@edit_ingredients_loading_photo_btn_text": { + "description": "Ingredients edition - Loading photo from the server" + }, "edit_ingredients_refresh_photo_btn_text": "Refresh photo", "@edit_ingredients_refresh_photo_btn_text": { "description": "Ingredients edition - Refresh photo" }, - "edit_packaging_extract_btn_text": "Extract packaging", + "edit_packaging_extract_btn_text": "Extract packaging\nfrom the photo", "@edit_packaging_extract_btn_text": { "description": "Packaging edition - OCR-Extract packaging" }, + "edit_packaging_extracting_btn_text": "Extracting packaging from the photo", + "@edit_packaging_extracting_btn_text": { + "description": "Packaging edition - OCR-Extracting packaging" + }, + "edit_packaging_loading_photo_btn_text": "Loading photo…", + "@edit_packaging_loading_photo_btn_text": { + "description": "Packaging edition - Loading photo from the server" + }, "edit_packaging_refresh_photo_btn_text": "Refresh photo", "@edit_packaging_refresh_photo_btn_text": { "description": "Packaging edition - Refresh photo" diff --git a/packages/smooth_app/lib/pages/product/edit_ocr_page.dart b/packages/smooth_app/lib/pages/product/edit_ocr_page.dart index 4331a249eea..d9d48af0e93 100644 --- a/packages/smooth_app/lib/pages/product/edit_ocr_page.dart +++ b/packages/smooth_app/lib/pages/product/edit_ocr_page.dart @@ -2,6 +2,7 @@ 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:smooth_app/background/background_task_details.dart'; import 'package:smooth_app/data_models/up_to_date_mixin.dart'; import 'package:smooth_app/database/local_database.dart'; @@ -18,6 +19,7 @@ import 'package:smooth_app/pages/product/explanation_widget.dart'; import 'package:smooth_app/pages/product/multilingual_helper.dart'; import 'package:smooth_app/pages/product/ocr_helper.dart'; import 'package:smooth_app/pages/product/product_image_button.dart'; +import 'package:smooth_app/themes/smooth_theme_colors.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; /// Editing with OCR a product field and the corresponding image. @@ -43,6 +45,7 @@ class _EditOcrPageState extends State with UpToDateMixin { late final MultilingualHelper _multilingualHelper; OcrHelper get _helper => widget.helper; + bool _extractingData = false; @override void initState() { @@ -62,7 +65,8 @@ class _EditOcrPageState extends State with UpToDateMixin { /// /// When done, populates the related page field. Future _extractData() async { - // TODO(monsieurtanuki): hide the "extract" button while extracting, or display a loading dialog on top + setState(() => _extractingData = true); + try { final String? extractedText = await _helper.getExtractedText( widget.product, @@ -72,6 +76,8 @@ class _EditOcrPageState extends State with UpToDateMixin { return; } + setState(() => _extractingData = false); + if (extractedText == null || extractedText.isEmpty) { await LoadingDialog.error( context: context, @@ -83,9 +89,7 @@ class _EditOcrPageState extends State with UpToDateMixin { if (_controller.text != extractedText) { setState(() => _controller.text = extractedText); } - } catch (e) { - // - } + } catch (_) {} } /// Updates the product field on the server. @@ -282,17 +286,11 @@ class _EditOcrPageState extends State with UpToDateMixin { setState: setState, product: upToDateProduct, ), - if (transientFile.isServerImage()) - SmoothActionButtonsBar.single( - action: SmoothActionButton( - text: - _helper.getActionExtractText(appLocalizations), - onPressed: () async => _extractData(), - ), - ) - else if (transientFile.isImageAvailable()) - // TODO(monsieurtanuki): what if slow upload? text instead? - const CircularProgressIndicator.adaptive(), + _EditOcrMainAction( + onPressed: _extractData, + helper: _helper, + state: _extractState(transientFile), + ), const SizedBox(height: MEDIUM_SPACE), TextField( controller: _controller, @@ -377,4 +375,224 @@ class _EditOcrPageState extends State with UpToDateMixin { } return result; } + + OcrState _extractState(TransientFile transientFile) { + if (_extractingData) { + return OcrState.EXTRACTING_DATA; + } else if (transientFile.isServerImage()) { + return OcrState.IMAGE_LOADED; + } else if (transientFile.isImageAvailable()) { + return OcrState.IMAGE_LOADING; + } else { + return OcrState.OTHER; + } + } +} + +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 CircularProgressIndicator.adaptive(), + Expanded( + child: Text( + helper.getActionExtractingData(appLocalizations), + textAlign: TextAlign.center, + ), + ), + const CircularProgressIndicator.adaptive(), + ], + ), + ), + ), + ); + } +} + +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 EdgeInsets.symmetric(horizontal: MEDIUM_SPACE), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Text( + helper.getActionLoadingPhoto(appLocalizations), + ), + ), + const CircularProgressIndicator.adaptive(), + ], + ), + ); + } +} + +enum OcrState { + IMAGE_LOADING, + IMAGE_LOADED, + EXTRACTING_DATA, + OTHER, } diff --git a/packages/smooth_app/lib/pages/product/ocr_helper.dart b/packages/smooth_app/lib/pages/product/ocr_helper.dart index 61b5b2cc33c..2a232e05e04 100644 --- a/packages/smooth_app/lib/pages/product/ocr_helper.dart +++ b/packages/smooth_app/lib/pages/product/ocr_helper.dart @@ -39,6 +39,12 @@ abstract class OcrHelper { /// Returns the "extract text" button label String getActionExtractText(final AppLocalizations appLocalizations); + /// Returns the "loading photo" button label + String getActionLoadingPhoto(final AppLocalizations appLocalizations); + + /// Returns the "Extracting data…" button label + String getActionExtractingData(final AppLocalizations appLocalizations); + /// Returns the "refresh photo" button label String getActionRefreshPhoto(final AppLocalizations appLocalizations); diff --git a/packages/smooth_app/lib/pages/product/ocr_ingredients_helper.dart b/packages/smooth_app/lib/pages/product/ocr_ingredients_helper.dart index 6f1eeb8a48d..1421d5a70d8 100644 --- a/packages/smooth_app/lib/pages/product/ocr_ingredients_helper.dart +++ b/packages/smooth_app/lib/pages/product/ocr_ingredients_helper.dart @@ -43,7 +43,15 @@ class OcrIngredientsHelper extends OcrHelper { @override String getActionExtractText(final AppLocalizations appLocalizations) => - appLocalizations.edit_ingredients_extrait_ingredients_btn_text; + appLocalizations.edit_ingredients_extract_ingredients_btn_text; + + @override + String getActionExtractingData(AppLocalizations appLocalizations) => + appLocalizations.edit_ingredients_extracting_ingredients_btn_text; + + @override + String getActionLoadingPhoto(AppLocalizations appLocalizations) => + appLocalizations.edit_ingredients_loading_photo_btn_text; @override String getActionRefreshPhoto(final AppLocalizations appLocalizations) => diff --git a/packages/smooth_app/lib/pages/product/ocr_packaging_helper.dart b/packages/smooth_app/lib/pages/product/ocr_packaging_helper.dart index 179adefe296..600e24a7563 100644 --- a/packages/smooth_app/lib/pages/product/ocr_packaging_helper.dart +++ b/packages/smooth_app/lib/pages/product/ocr_packaging_helper.dart @@ -48,6 +48,14 @@ class OcrPackagingHelper extends OcrHelper { String getActionExtractText(final AppLocalizations appLocalizations) => appLocalizations.edit_packaging_extract_btn_text; + @override + String getActionExtractingData(AppLocalizations appLocalizations) => + appLocalizations.edit_packaging_extracting_btn_text; + + @override + String getActionLoadingPhoto(AppLocalizations appLocalizations) => + appLocalizations.edit_packaging_loading_photo_btn_text; + @override String getActionRefreshPhoto(final AppLocalizations appLocalizations) => appLocalizations.edit_packaging_refresh_photo_btn_text;