Skip to content

Commit

Permalink
feat: prices - barcode reader for additional products (openfoodfacts#…
Browse files Browse the repository at this point in the history
…5381)

New file:
* `price_scan_page.dart`: Page showing the camera feed and decoding the first barcode, for Prices.

Impacted files:
* `app_en.arb`: added 1 "barcode reader" label
* `app_fr.arb`: added 1 "barcode reader" label
* `camera_scan_page.dart`: minor refactoring
* `price_product_search_page.dart`: added a FAB towards the new barcode reader page
  • Loading branch information
monsieurtanuki authored Jun 15, 2024
1 parent 2a08ab4 commit 35a4ab0
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 56 deletions.
1 change: 1 addition & 0 deletions packages/smooth_app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
1 change: 1 addition & 0 deletions packages/smooth_app/lib/l10n/app_fr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
115 changes: 74 additions & 41 deletions packages/smooth_app/lib/pages/prices/price_product_search_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -48,14 +50,20 @@ class _PriceProductSearchPageState extends State<PriceProductSearchPage> {
@override
Widget build(BuildContext context) {
final AppLocalizations appLocalizations = AppLocalizations.of(context);
final LocalDatabase localDatabase = context.read<LocalDatabase>();
// TODO(monsieurtanuki): add WillPopScope2
return SmoothScaffold(
appBar: SmoothAppBar(
centerTitle: false,
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(
Expand All @@ -67,46 +75,8 @@ class _PriceProductSearchPageState extends State<PriceProductSearchPage> {
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,
),
Expand Down Expand Up @@ -175,4 +145,67 @@ class _PriceProductSearchPageState extends State<PriceProductSearchPage> {
}
return buffer.toString();
}

Future<void> _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<LocalDatabase>();
final Product? product = await _localSearch(
barcode,
localDatabase,
);
if (product != null) {
setState(() => _product = PriceMetaProduct.product(product));
return;
}
}

Future<void> _onFieldSubmitted(final BuildContext context) async {
final String barcode = _controller.text;
if (barcode.isEmpty) {
return;
}

final LocalDatabase localDatabase = context.read<LocalDatabase>();
final Product? product = await _serverSearch(
barcode,
localDatabase,
context,
);
if (product != null) {
setState(() => _product = PriceMetaProduct.product(product));
}
}

Future<void> _scan(final BuildContext context) async {
final String? barcode = await Navigator.of(context).push<String>(
MaterialPageRoute<String>(
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);
}
}
53 changes: 53 additions & 0 deletions packages/smooth_app/lib/pages/prices/price_scan_page.dart
Original file line number Diff line number Diff line change
@@ -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<PriceScanPage> createState() => _PriceScanPageState();
}

class _PriceScanPageState extends State<PriceScanPage>
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,
),
);
}
}
29 changes: 14 additions & 15 deletions packages/smooth_app/lib/pages/scan/camera_scan_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,21 @@ class CameraScannerPage extends StatefulWidget {
const CameraScannerPage();

@override
CameraScannerPageState createState() => CameraScannerPageState();
State<CameraScannerPage> createState() => _CameraScannerPageState();

static Future<void> onCameraFlashError(BuildContext context) async {
final AppLocalizations appLocalizations = AppLocalizations.of(context);
return showDialog<void>(
context: context,
builder: (_) => SmoothAlertDialog(
title: appLocalizations.camera_flash_error_dialog_title,
body: Text(appLocalizations.camera_flash_error_dialog_message),
),
);
}
}

class CameraScannerPageState extends State<CameraScannerPage>
class _CameraScannerPageState extends State<CameraScannerPage>
with TraceableClientMixin {
final GlobalKey<State<StatefulWidget>> _headerKey = GlobalKey();

Expand Down Expand Up @@ -87,7 +98,7 @@ class CameraScannerPageState extends State<CameraScannerPage>
GlobalVars.barcodeScanner.getScanner(
onScan: _onNewBarcodeDetected,
hapticFeedback: () => SmoothHapticFeedback.click(),
onCameraFlashError: _onCameraFlashError,
onCameraFlashError: CameraScannerPage.onCameraFlashError,
trackCustomEvent: AnalyticsHelper.trackCustomEvent,
hasMoreThanOneCamera: CameraHelper.hasMoreThanOneCamera,
toggleCameraModeTooltip: appLocalizations.camera_toggle_camera,
Expand Down Expand Up @@ -115,16 +126,4 @@ class CameraScannerPageState extends State<CameraScannerPage>
_userPreferences.incrementScanCount();
return true;
}

void _onCameraFlashError(BuildContext context) {
final AppLocalizations appLocalizations = AppLocalizations.of(context);

showDialog<void>(
context: context,
builder: (_) => SmoothAlertDialog(
title: appLocalizations.camera_flash_error_dialog_title,
body: Text(appLocalizations.camera_flash_error_dialog_message),
),
);
}
}

0 comments on commit 35a4ab0

Please sign in to comment.