From fdf1d03f10fe8a9cf517fb5c6a157b4ffd2563d2 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Fri, 20 Dec 2024 16:24:27 +0100 Subject: [PATCH] feat: 5594 - multi product scan mode as a dev mode option for price receipt input (#6008) Impacted files: * `price_add_product_card.dart`: now calling the scan card with the bool multi product scan mode parameter, and manages a _list_ of scanned barcodes * `price_scan_page.dart`: now manages a multi product scan mode, with floating action button and snackbar * `user_preferences_dev_mode.dart`: new flag for "multi product scan in price receipt" --- .../user_preferences_dev_mode.dart | 15 +++ .../pages/prices/price_add_product_card.dart | 103 +++++++++++++----- .../lib/pages/prices/price_scan_page.dart | 60 +++++++++- 3 files changed, 143 insertions(+), 35 deletions(-) diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_dev_mode.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_dev_mode.dart index c393ac28a21..35ba88506c9 100644 --- a/packages/smooth_app/lib/pages/preferences/user_preferences_dev_mode.dart +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_dev_mode.dart @@ -55,6 +55,8 @@ class UserPreferencesDevMode extends AbstractUserPreferences { static const String userPreferencesFlagAccessibilityEmoji = '__accessibilityEmoji'; static const String userPreferencesFlagUserOrderedKP = '__userOrderedKP'; + static const String userPreferencesFlagPricesReceiptMultiSelection = + '__pricesReceiptMultiSelection'; static const String userPreferencesFlagSpellCheckerOnOcr = '__spellcheckerOcr'; static const String userPreferencesCustomNewsJSONURI = '__newsJsonURI'; @@ -431,6 +433,19 @@ class UserPreferencesDevMode extends AbstractUserPreferences { UserPreferencesItemSection( label: appLocalizations.dev_mode_section_experimental_features, ), + UserPreferencesItemSwitch( + title: 'Multi-products selection for prices', + value: userPreferences + .getFlag(userPreferencesFlagPricesReceiptMultiSelection) ?? + false, + onChanged: (bool value) async { + await userPreferences.setFlag( + userPreferencesFlagPricesReceiptMultiSelection, + value, + ); + _showSuccessMessage(); + }, + ), UserPreferencesItemSwitch( title: 'User ordered knowledge panels', value: userPreferences.getFlag(userPreferencesFlagUserOrderedKP) ?? diff --git a/packages/smooth_app/lib/pages/prices/price_add_product_card.dart b/packages/smooth_app/lib/pages/prices/price_add_product_card.dart index 1a8c709293c..2d4c29c8682 100644 --- a/packages/smooth_app/lib/pages/prices/price_add_product_card.dart +++ b/packages/smooth_app/lib/pages/prices/price_add_product_card.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; +import 'package:smooth_app/data_models/preferences/user_preferences.dart'; import 'package:smooth_app/database/local_database.dart'; import 'package:smooth_app/generic_lib/buttons/smooth_large_button_with_icon.dart'; import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_card.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_text_form_field.dart'; +import 'package:smooth_app/pages/preferences/user_preferences_dev_mode.dart'; import 'package:smooth_app/pages/prices/price_amount_model.dart'; import 'package:smooth_app/pages/prices/price_meta_product.dart'; import 'package:smooth_app/pages/prices/price_model.dart'; @@ -50,21 +52,29 @@ class _PriceAddProductCardState extends State { text: appLocalizations.prices_barcode_reader_action, icon: Icons.barcode_reader, onPressed: () async { - final String? barcode = await Navigator.of(context).push( - MaterialPageRoute( + final UserPreferences userPreferences = + context.read(); + final List? barcodes = + await Navigator.of(context).push>( + MaterialPageRoute>( builder: (BuildContext context) => PriceScanPage( latestScannedBarcode: _latestScannedBarcode, + isMultiProducts: userPreferences.getFlag( + UserPreferencesDevMode + .userPreferencesFlagPricesReceiptMultiSelection, + ) ?? + false, ), ), ); - if (barcode == null) { + if (barcodes == null || barcodes.isEmpty) { return; } - _latestScannedBarcode = barcode; + _latestScannedBarcode = barcodes.last; if (!context.mounted) { return; } - await _addToList(barcode, context); + await _addBarcodesToList(barcodes, context); }, ), SmoothLargeButtonWithIcon( @@ -75,10 +85,11 @@ class _PriceAddProductCardState extends State { if (barcode == null) { return; } + _latestScannedBarcode = null; if (!context.mounted) { return; } - await _addToList(barcode, context); + await _addBarcodesToList([barcode], context); }, ), ], @@ -86,8 +97,8 @@ class _PriceAddProductCardState extends State { ); } - Future _addToList( - final String barcode, + Future _addBarcodesToList( + final List barcodes, final BuildContext context, ) async { final AppLocalizations appLocalizations = AppLocalizations.of(context); @@ -96,39 +107,71 @@ class _PriceAddProductCardState extends State { context, listen: false, ); - for (int i = 0; i < priceModel.length; i++) { - final PriceAmountModel model = priceModel.elementAt(i); - if (model.product.barcode == barcode) { - await showDialog( - context: context, - builder: (final BuildContext context) => SmoothAlertDialog( - body: Text(appLocalizations.prices_barcode_already(barcode)), - positiveAction: SmoothActionButton( - text: appLocalizations.okay, - onPressed: () => Navigator.of(context).pop(), - ), + + bool barcodeAlreadyThere(final String barcode) { + for (int i = 0; i < priceModel.length; i++) { + final PriceAmountModel model = priceModel.elementAt(i); + if (model.product.barcode == barcode) { + return true; + } + } + return false; + } + + final List alreadyThere = []; + final List notThere = []; + for (final String barcode in barcodes) { + if (barcodeAlreadyThere(barcode)) { + alreadyThere.add(barcode); + } else { + notThere.add(barcode); + } + } + + if (notThere.isNotEmpty) { + for (final String barcode in notThere) { + _addProductToList( + priceModel, + PriceMetaProduct.unknown( + barcode, + localDatabase, + priceModel, ), + context, ); - return; } + priceModel.notifyListeners(); } - priceModel.add( - PriceAmountModel( - product: PriceMetaProduct.unknown( - barcode, - localDatabase, - priceModel, + + for (final String barcode in alreadyThere) { + if (!context.mounted) { + return; + } + await showDialog( + context: context, + builder: (final BuildContext context) => SmoothAlertDialog( + body: Text(appLocalizations.prices_barcode_already(barcode)), + positiveAction: SmoothActionButton( + text: appLocalizations.okay, + onPressed: () => Navigator.of(context).pop(), + ), ), - ), - ); + ); + } + } + + void _addProductToList( + final PriceModel priceModel, + final PriceMetaProduct product, + final BuildContext context, + ) { + priceModel.add(PriceAmountModel(product: product)); // unfocus from the previous price amount text field. // looks like the most efficient way to unfocus: focus somewhere in space... final FocusNode focusNode = FocusNode(); _dummyFocusNodes.add(focusNode); FocusScope.of(context).requestFocus(focusNode); - - priceModel.notifyListeners(); } Future _textInput(final BuildContext context) async { diff --git a/packages/smooth_app/lib/pages/prices/price_scan_page.dart b/packages/smooth_app/lib/pages/prices/price_scan_page.dart index b5627538f20..3d785672f02 100644 --- a/packages/smooth_app/lib/pages/prices/price_scan_page.dart +++ b/packages/smooth_app/lib/pages/prices/price_scan_page.dart @@ -1,19 +1,27 @@ +import 'dart:async'; + 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/generic_lib/duration_constants.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_app_bar.dart'; +import 'package:smooth_app/widgets/smooth_floating_message.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({required this.latestScannedBarcode}); + const PriceScanPage({ + required this.latestScannedBarcode, + required this.isMultiProducts, + }); final String? latestScannedBarcode; + final bool isMultiProducts; @override State createState() => _PriceScanPageState(); @@ -26,10 +34,19 @@ class _PriceScanPageState extends State // `Failed assertion: line 5277 pos 12: '!_debugLocked': is not true.` bool _mutex = false; + final List _barcodes = []; + late String? _latestScannedBarcode; + @override String get actionName => 'Opened ${GlobalVars.barcodeScanner.getType()}_page for price'; + @override + void initState() { + super.initState(); + _latestScannedBarcode = widget.latestScannedBarcode; + } + @override Widget build(BuildContext context) { final AppLocalizations appLocalizations = AppLocalizations.of(context); @@ -37,18 +54,47 @@ class _PriceScanPageState extends State appBar: SmoothAppBar( title: Text(appLocalizations.prices_add_an_item), ), + floatingActionButton: !widget.isMultiProducts + ? null + : _barcodes.isEmpty + ? null + : FloatingActionButton.extended( + onPressed: () => _pop(context), + label: + Text(appLocalizations.user_list_length(_barcodes.length)), + icon: const Icon(Icons.add), + ), body: GlobalVars.barcodeScanner.getScanner( onScan: (final String barcode) async { // for some reason, the scanner sometimes returns immediately the // previously scanned barcode. - if (widget.latestScannedBarcode == barcode) { + if (_latestScannedBarcode == barcode) { return false; } - if (_mutex) { + _latestScannedBarcode = barcode; + if (_barcodes.contains(barcode)) { return false; } - _mutex = true; - Navigator.of(context).pop(barcode); + if (!widget.isMultiProducts) { + if (_mutex) { + return false; + } + _mutex = true; + } + _barcodes.add(barcode); + if (!widget.isMultiProducts) { + _pop(context); + return true; + } + SmoothFloatingMessage( + message: appLocalizations.scan_announce_new_barcode(barcode), + ).show( + context, + duration: SnackBarDuration.medium, + alignment: const Alignment(0.0, -0.75), + ); + unawaited(SmoothHapticFeedback.click()); + setState(() {}); return true; }, hapticFeedback: () => SmoothHapticFeedback.click(), @@ -61,4 +107,8 @@ class _PriceScanPageState extends State ), ); } + + void _pop(final BuildContext context) { + Navigator.of(context).pop(_barcodes); + } }