diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index 8c378f6d3d2..7c55331791d 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -1688,6 +1688,7 @@ } } }, + "prices_barcode_reader_action": "Barcode reader", "prices_view_prices": "View the prices", "prices_list_length_one_page": "{count,plural, =0{No price yet} =1{Only one price} other{All {count} prices}}", "@prices_list_length_one_page": { diff --git a/packages/smooth_app/lib/l10n/app_fr.arb b/packages/smooth_app/lib/l10n/app_fr.arb index bde1e1bf2ba..1b8811a31c8 100644 --- a/packages/smooth_app/lib/l10n/app_fr.arb +++ b/packages/smooth_app/lib/l10n/app_fr.arb @@ -1688,6 +1688,7 @@ } } }, + "prices_barcode_reader_action": "Lecteur de code-barres", "prices_view_prices": "Voir les prix", "prices_list_length_one_page": "{count,plural, =0{Aucun prix} =1{Un seul prix} other{Tous les {count} prix}}", "@prices_list_length_one_page": { diff --git a/packages/smooth_app/lib/pages/prices/price_product_search_page.dart b/packages/smooth_app/lib/pages/prices/price_product_search_page.dart index 5ec45641130..74a4933cb59 100644 --- a/packages/smooth_app/lib/pages/prices/price_product_search_page.dart +++ b/packages/smooth_app/lib/pages/prices/price_product_search_page.dart @@ -10,8 +10,10 @@ import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/loading_dialog.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_back_button.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_text_form_field.dart'; +import 'package:smooth_app/helpers/camera_helper.dart'; import 'package:smooth_app/pages/prices/price_meta_product.dart'; import 'package:smooth_app/pages/prices/price_product_list_tile.dart'; +import 'package:smooth_app/pages/prices/price_scan_page.dart'; import 'package:smooth_app/pages/product/common/product_refresher.dart'; import 'package:smooth_app/widgets/smooth_app_bar.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; @@ -48,7 +50,6 @@ class _PriceProductSearchPageState extends State { @override Widget build(BuildContext context) { final AppLocalizations appLocalizations = AppLocalizations.of(context); - final LocalDatabase localDatabase = context.read(); // TODO(monsieurtanuki): add WillPopScope2 return SmoothScaffold( appBar: SmoothAppBar( @@ -56,6 +57,13 @@ class _PriceProductSearchPageState extends State { leading: const SmoothBackButton(), title: Text(appLocalizations.prices_barcode_search_title), ), + floatingActionButton: !CameraHelper.hasACamera + ? null + : FloatingActionButton.extended( + onPressed: () async => _scan(context), + label: Text(appLocalizations.prices_barcode_reader_action), + icon: const Icon(Icons.barcode_reader), + ), body: SingleChildScrollView( padding: const EdgeInsets.all(LARGE_SPACE), child: Column( @@ -67,46 +75,8 @@ class _PriceProductSearchPageState extends State { controller: _controller, hintText: _barcodeHint, textInputType: _textInputType, - onChanged: (_) async { - final String barcode = _controller.text; - final String cleanBarcode = _getCleanBarcode(barcode); - if (barcode != cleanBarcode) { - setState(() => _controller.text = cleanBarcode); - return; - } - - if (_product != null) { - setState( - () => _product = null, - ); - } - - final Product? product = await _localSearch( - barcode, - localDatabase, - ); - if (product != null) { - setState( - () => _product = PriceMetaProduct.product(product), - ); - return; - } - }, - onFieldSubmitted: (_) async { - final String barcode = _controller.text; - if (barcode.isEmpty) { - return; - } - - final Product? product = await _serverSearch( - barcode, - localDatabase, - context, - ); - if (product != null) { - setState(() => _product = PriceMetaProduct.product(product)); - } - }, + onChanged: (_) async => _onChanged(context), + onFieldSubmitted: (_) async => _onFieldSubmitted(context), prefixIcon: const Icon(CupertinoIcons.barcode), textInputAction: TextInputAction.search, ), @@ -175,4 +145,67 @@ class _PriceProductSearchPageState extends State { } return buffer.toString(); } + + Future _onChanged(final BuildContext context) async { + final String barcode = _controller.text; + final String cleanBarcode = _getCleanBarcode(barcode); + if (barcode != cleanBarcode) { + setState(() => _controller.text = cleanBarcode); + return; + } + + if (_product != null) { + setState(() => _product = null); + } + + final LocalDatabase localDatabase = context.read(); + final Product? product = await _localSearch( + barcode, + localDatabase, + ); + if (product != null) { + setState(() => _product = PriceMetaProduct.product(product)); + return; + } + } + + Future _onFieldSubmitted(final BuildContext context) async { + final String barcode = _controller.text; + if (barcode.isEmpty) { + return; + } + + final LocalDatabase localDatabase = context.read(); + final Product? product = await _serverSearch( + barcode, + localDatabase, + context, + ); + if (product != null) { + setState(() => _product = PriceMetaProduct.product(product)); + } + } + + Future _scan(final BuildContext context) async { + final String? barcode = await Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => const PriceScanPage(), + ), + ); + if (barcode == null) { + return; + } + _controller.text = barcode; + if (!context.mounted) { + return; + } + await _onChanged(context); + if (_product != null) { + return; + } + if (!context.mounted) { + return; + } + await _onFieldSubmitted(context); + } } diff --git a/packages/smooth_app/lib/pages/prices/price_scan_page.dart b/packages/smooth_app/lib/pages/prices/price_scan_page.dart new file mode 100644 index 00000000000..1b12cabd097 --- /dev/null +++ b/packages/smooth_app/lib/pages/prices/price_scan_page.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:matomo_tracker/matomo_tracker.dart'; +import 'package:smooth_app/helpers/analytics_helper.dart'; +import 'package:smooth_app/helpers/camera_helper.dart'; +import 'package:smooth_app/helpers/global_vars.dart'; +import 'package:smooth_app/helpers/haptic_feedback_helper.dart'; +import 'package:smooth_app/pages/scan/camera_scan_page.dart'; +import 'package:smooth_app/widgets/smooth_scaffold.dart'; + +/// Page showing the camera feed and decoding the first barcode, for Prices. +class PriceScanPage extends StatefulWidget { + const PriceScanPage(); + + @override + State createState() => _PriceScanPageState(); +} + +class _PriceScanPageState extends State + with TraceableClientMixin { + // Mutual exclusion needed: we typically receive several times the same + // barcode and the `pop` would be called several times and cause an error like + // `Failed assertion: line 5277 pos 12: '!_debugLocked': is not true.` + bool _mutex = false; + + @override + String get actionName => + 'Opened ${GlobalVars.barcodeScanner.getType()}_page for price'; + + @override + Widget build(BuildContext context) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + return SmoothScaffold( + body: GlobalVars.barcodeScanner.getScanner( + onScan: (final String barcode) async { + if (_mutex) { + return false; + } + _mutex = true; + Navigator.of(context).pop(barcode); + return true; + }, + hapticFeedback: () => SmoothHapticFeedback.click(), + onCameraFlashError: CameraScannerPage.onCameraFlashError, + trackCustomEvent: AnalyticsHelper.trackCustomEvent, + hasMoreThanOneCamera: CameraHelper.hasMoreThanOneCamera, + toggleCameraModeTooltip: appLocalizations.camera_toggle_camera, + toggleFlashModeTooltip: appLocalizations.camera_toggle_flash, + contentPadding: null, + ), + ); + } +} diff --git a/packages/smooth_app/lib/pages/scan/camera_scan_page.dart b/packages/smooth_app/lib/pages/scan/camera_scan_page.dart index f9442cae2cf..4a6af676926 100644 --- a/packages/smooth_app/lib/pages/scan/camera_scan_page.dart +++ b/packages/smooth_app/lib/pages/scan/camera_scan_page.dart @@ -19,10 +19,21 @@ class CameraScannerPage extends StatefulWidget { const CameraScannerPage(); @override - CameraScannerPageState createState() => CameraScannerPageState(); + State createState() => _CameraScannerPageState(); + + static Future onCameraFlashError(BuildContext context) async { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + return showDialog( + context: context, + builder: (_) => SmoothAlertDialog( + title: appLocalizations.camera_flash_error_dialog_title, + body: Text(appLocalizations.camera_flash_error_dialog_message), + ), + ); + } } -class CameraScannerPageState extends State +class _CameraScannerPageState extends State with TraceableClientMixin { final GlobalKey> _headerKey = GlobalKey(); @@ -87,7 +98,7 @@ class CameraScannerPageState extends State GlobalVars.barcodeScanner.getScanner( onScan: _onNewBarcodeDetected, hapticFeedback: () => SmoothHapticFeedback.click(), - onCameraFlashError: _onCameraFlashError, + onCameraFlashError: CameraScannerPage.onCameraFlashError, trackCustomEvent: AnalyticsHelper.trackCustomEvent, hasMoreThanOneCamera: CameraHelper.hasMoreThanOneCamera, toggleCameraModeTooltip: appLocalizations.camera_toggle_camera, @@ -115,16 +126,4 @@ class CameraScannerPageState extends State _userPreferences.incrementScanCount(); return true; } - - void _onCameraFlashError(BuildContext context) { - final AppLocalizations appLocalizations = AppLocalizations.of(context); - - showDialog( - context: context, - builder: (_) => SmoothAlertDialog( - title: appLocalizations.camera_flash_error_dialog_title, - body: Text(appLocalizations.camera_flash_error_dialog_message), - ), - ); - } }