Skip to content

Commit

Permalink
feat: 5594 - multi product scan mode as a dev mode option for price r…
Browse files Browse the repository at this point in the history
…eceipt 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"
  • Loading branch information
monsieurtanuki authored Dec 20, 2024
1 parent a7a9990 commit fdf1d03
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) ??
Expand Down
103 changes: 73 additions & 30 deletions packages/smooth_app/lib/pages/prices/price_add_product_card.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -50,21 +52,29 @@ class _PriceAddProductCardState extends State<PriceAddProductCard> {
text: appLocalizations.prices_barcode_reader_action,
icon: Icons.barcode_reader,
onPressed: () async {
final String? barcode = await Navigator.of(context).push<String>(
MaterialPageRoute<String>(
final UserPreferences userPreferences =
context.read<UserPreferences>();
final List<String>? barcodes =
await Navigator.of(context).push<List<String>>(
MaterialPageRoute<List<String>>(
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(
Expand All @@ -75,19 +85,20 @@ class _PriceAddProductCardState extends State<PriceAddProductCard> {
if (barcode == null) {
return;
}
_latestScannedBarcode = null;
if (!context.mounted) {
return;
}
await _addToList(barcode, context);
await _addBarcodesToList(<String>[barcode], context);
},
),
],
),
);
}

Future<void> _addToList(
final String barcode,
Future<void> _addBarcodesToList(
final List<String> barcodes,
final BuildContext context,
) async {
final AppLocalizations appLocalizations = AppLocalizations.of(context);
Expand All @@ -96,39 +107,71 @@ class _PriceAddProductCardState extends State<PriceAddProductCard> {
context,
listen: false,
);
for (int i = 0; i < priceModel.length; i++) {
final PriceAmountModel model = priceModel.elementAt(i);
if (model.product.barcode == barcode) {
await showDialog<void>(
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<String> alreadyThere = <String>[];
final List<String> notThere = <String>[];
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<void>(
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<String?> _textInput(final BuildContext context) async {
Expand Down
60 changes: 55 additions & 5 deletions packages/smooth_app/lib/pages/prices/price_scan_page.dart
Original file line number Diff line number Diff line change
@@ -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<PriceScanPage> createState() => _PriceScanPageState();
Expand All @@ -26,29 +34,67 @@ class _PriceScanPageState extends State<PriceScanPage>
// `Failed assertion: line 5277 pos 12: '!_debugLocked': is not true.`
bool _mutex = false;

final List<String> _barcodes = <String>[];
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);
return SmoothScaffold(
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(),
Expand All @@ -61,4 +107,8 @@ class _PriceScanPageState extends State<PriceScanPage>
),
);
}

void _pop(final BuildContext context) {
Navigator.of(context).pop(_barcodes);
}
}

0 comments on commit fdf1d03

Please sign in to comment.