From b051aeccda657e01692ab816dd5a50f1093315b1 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 11 Jun 2023 09:41:26 +1000 Subject: [PATCH] Barcode refactor (#363) * Move barcode.dart * Fix * Refactoring barcode scanner code: - Abstract the "controller" class (for future development) - Break barcode scanning code out into multiple files - Add CameraBarcodeController class (qr_code_scanner) * Add await * Make barcode scan delay configurable * remove unused import * Handle camera exceptions * Improve sequencing for camera scanner - Show loading overlay - Prevent reload if view is no longer mounted * Update docstring * Update release notes --- assets/release_notes.md | 6 + lib/api_form.dart | 5 +- lib/{ => barcode}/barcode.dart | 309 ++------------------------- lib/barcode/camera_controller.dart | 154 +++++++++++++ lib/barcode/controller.dart | 108 ++++++++++ lib/barcode/handler.dart | 121 +++++++++++ lib/barcode/tones.dart | 23 ++ lib/l10n/app_en.arb | 12 ++ lib/preferences.dart | 3 + lib/settings/barcode_settings.dart | 114 ++++++++++ lib/settings/settings.dart | 10 + lib/widget/location_display.dart | 8 +- lib/widget/part_detail.dart | 2 +- lib/widget/refreshable_state.dart | 2 +- lib/widget/stock_detail.dart | 7 +- lib/widget/supplier_part_detail.dart | 2 +- test/barcode_test.dart | 18 +- 17 files changed, 592 insertions(+), 312 deletions(-) rename lib/{ => barcode}/barcode.dart (64%) create mode 100644 lib/barcode/camera_controller.dart create mode 100644 lib/barcode/controller.dart create mode 100644 lib/barcode/handler.dart create mode 100644 lib/barcode/tones.dart create mode 100644 lib/settings/barcode_settings.dart diff --git a/assets/release_notes.md b/assets/release_notes.md index 10350fc5..3aa2c2b4 100644 --- a/assets/release_notes.md +++ b/assets/release_notes.md @@ -1,3 +1,9 @@ +### - +--- +- Improvements to barcode scanning +- Translation updates + + ### 0.12.1 - May 2023 - Fixes bug in purchase order form diff --git a/lib/api_form.dart b/lib/api_form.dart index 83cf3e8c..f8d82e07 100644 --- a/lib/api_form.dart +++ b/lib/api_form.dart @@ -9,7 +9,8 @@ import "package:flutter/material.dart"; import "package:inventree/api.dart"; import "package:inventree/app_colors.dart"; -import "package:inventree/barcode.dart"; +import "package:inventree/barcode/barcode.dart"; +import "package:inventree/barcode/tones.dart"; import "package:inventree/helpers.dart"; import "package:inventree/l10.dart"; @@ -349,7 +350,7 @@ class APIFormField { Navigator.push( context, - MaterialPageRoute(builder: (context) => InvenTreeQRView(handler) + MaterialPageRoute(builder: (context) => barcodeController(handler) ) ); }, diff --git a/lib/barcode.dart b/lib/barcode/barcode.dart similarity index 64% rename from lib/barcode.dart rename to lib/barcode/barcode.dart index 5487e3ee..6c3c592b 100644 --- a/lib/barcode.dart +++ b/lib/barcode/barcode.dart @@ -1,155 +1,42 @@ -import "dart:io"; import "package:flutter/material.dart"; + import "package:flutter_speed_dial/flutter_speed_dial.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; -import "package:inventree/inventree/purchase_order.dart"; -import "package:inventree/widget/purchase_order_detail.dart"; import "package:one_context/one_context.dart"; -import "package:qr_code_scanner/qr_code_scanner.dart"; + import "package:inventree/api.dart"; import "package:inventree/helpers.dart"; import "package:inventree/l10.dart"; -import "package:inventree/preferences.dart"; + +import "package:inventree/barcode/camera_controller.dart"; +import "package:inventree/barcode/controller.dart"; +import "package:inventree/barcode/handler.dart"; +import "package:inventree/barcode/tones.dart"; import "package:inventree/inventree/company.dart"; import "package:inventree/inventree/part.dart"; -import "package:inventree/inventree/sentry.dart"; +import "package:inventree/inventree/purchase_order.dart"; import "package:inventree/inventree/stock.dart"; -import "package:inventree/widget/refreshable_state.dart"; -import "package:inventree/widget/supplier_part_detail.dart"; import "package:inventree/widget/dialogs.dart"; -import "package:inventree/widget/snacks.dart"; import "package:inventree/widget/location_display.dart"; import "package:inventree/widget/part_detail.dart"; +import "package:inventree/widget/purchase_order_detail.dart"; +import "package:inventree/widget/refreshable_state.dart"; +import "package:inventree/widget/snacks.dart"; import "package:inventree/widget/stock_detail.dart"; +import "package:inventree/widget/supplier_part_detail.dart"; /* - * Play an audible 'success' alert to the user. + * Return a new BarcodeController instance */ -Future barcodeSuccessTone() async { - - final bool en = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool; - - if (en) { - playAudioFile("sounds/barcode_scan.mp3"); - } +InvenTreeBarcodeController barcodeController(BarcodeHandler handler) { + // TODO: Make this configurable + return CameraBarcodeController(handler); } -Future barcodeFailureTone() async { - - final bool en = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool; - - if (en) { - playAudioFile("sounds/barcode_error.mp3"); - } -} - - -/* Generic class which "handles" a barcode, by communicating with the InvenTree server, - * and handling match / unknown / error cases. - * - * Override functionality of this class to perform custom actions, - * based on the response returned from the InvenTree server - */ -class BarcodeHandler { - - BarcodeHandler(); - - String getOverlayText(BuildContext context) => "Barcode Overlay"; - - Future onBarcodeMatched(Map data) async { - // Called when the server "matches" a barcode - // Override this function - } - - Future onBarcodeUnknown(Map data) async { - // Called when the server does not know about a barcode - // Override this function - - barcodeFailureTone(); - - showSnackIcon( - L10().barcodeNoMatch, - success: false, - icon: Icons.qr_code, - ); - } - - // Called when the server returns an unhandled response - Future onBarcodeUnhandled(Map data) async { - barcodeFailureTone(); - showServerError("barcode/", L10().responseUnknown, data.toString()); - } - - /* - * Base function to capture and process barcode data. - * - * Returns true only if the barcode scanner should remain open - */ - Future processBarcode(QRViewController? _controller, String barcode, {String url = "barcode/"}) async { - - debug("Scanned barcode data: '${barcode}'"); - - barcode = barcode.trim(); - - // Empty barcode is invalid - if (barcode.isEmpty) { - - barcodeFailureTone(); - - showSnackIcon( - L10().barcodeError, - icon: FontAwesomeIcons.circleExclamation, - success: false - ); - - return; - } - - var response = await InvenTreeAPI().post( - url, - body: { - "barcode": barcode, - }, - expectedStatusCode: null, // Do not show an error on "unexpected code" - ); - - debug("Barcode scan response" + response.data.toString()); - - Map data = response.asMap(); - - // Handle strange response from the server - if (!response.isValid() || !response.isMap()) { - onBarcodeUnknown({}); - - showSnackIcon(L10().serverError, success: false); - - // We want to know about this one! - await sentryReportMessage( - "BarcodeHandler.processBarcode returned unexpected value", - context: { - "data": response.data?.toString() ?? "null", - "barcode": barcode, - "url": url, - "statusCode": response.statusCode.toString(), - "valid": response.isValid().toString(), - "error": response.error, - "errorDetail": response.errorDetail, - "className": "${this}", - } - ); - } else if (data.containsKey("success")) { - await onBarcodeMatched(data); - } else if ((response.statusCode >= 400) || data.containsKey("error")) { - await onBarcodeUnknown(data); - } else { - await onBarcodeUnhandled(data); - } - } -} /* * Class for general barcode scanning. @@ -638,168 +525,8 @@ class UniqueBarcodeHandler extends BarcodeHandler { } -class InvenTreeQRView extends StatefulWidget { - - const InvenTreeQRView(this._handler, {Key? key}) : super(key: key); - - final BarcodeHandler _handler; - - @override - State createState() => _QRViewState(); -} - - -class _QRViewState extends State { - - _QRViewState() : super(); - - final GlobalKey qrKey = GlobalKey(debugLabel: "QR"); - - QRViewController? _controller; - - bool flash_status = false; - - bool currently_processing = false; - - Future updateFlashStatus() async { - final bool? status = await _controller?.getFlashStatus(); - - flash_status = status != null && status; - - // Reload - if (mounted) { - setState(() {}); - } - } - - // In order to get hot reload to work we need to pause the camera if the platform - // is android, or resume the camera if the platform is iOS. - @override - void reassemble() { - super.reassemble(); - - if (mounted) { - if (Platform.isAndroid) { - _controller!.pauseCamera(); - } - - _controller!.resumeCamera(); - } - } - - /* Callback function when the Barcode scanner view is initially created */ - void _onViewCreated(BuildContext context, QRViewController controller) { - _controller = controller; - - controller.scannedDataStream.listen((barcode) { - handleBarcode(barcode.code); - }); - } - - /* Handle scanned data */ - Future handleBarcode(String? data) async { - - // Empty or missing data, or we have navigated away - if (!mounted || data == null || data.isEmpty) { - return; - } - - // Currently processing a barcode - return! - if (currently_processing) { - return; - } - - setState(() { - currently_processing = true; - }); - - // Pause camera functionality until we are done processing - _controller?.pauseCamera(); - - // processBarcode returns true if the scanner window is to remain open - widget._handler.processBarcode(_controller, data).then((value) { - // Re-start the process after some delay - Future.delayed(Duration(milliseconds: 500)).then((value) { - if (mounted) { - _controller?.resumeCamera(); - currently_processing = false; - } - }); - }); - } - - @override - void dispose() { - _controller?.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - - return Scaffold( - appBar: AppBar( - title: Text(L10().scanBarcode), - actions: [ - IconButton( - icon: Icon(Icons.flip_camera_android), - onPressed: () { - _controller?.flipCamera(); - } - ), - IconButton( - icon: flash_status ? Icon(Icons.flash_off) : Icon(Icons.flash_on), - onPressed: () { - _controller?.toggleFlash(); - updateFlashStatus(); - }, - ) - ], - ), - body: Stack( - children: [ - Column( - children: [ - Expanded( - child: QRView( - key: qrKey, - onQRViewCreated: (QRViewController controller) { - _onViewCreated(context, controller); - }, - overlay: QrScannerOverlayShape( - borderColor: Colors.red, - borderRadius: 10, - borderLength: 30, - borderWidth: 10, - cutOutSize: 300, - ), - ) - ) - ] - ), - Center( - child: Column( - children: [ - Spacer(), - Padding( - child: Text(widget._handler.getOverlayText(context), - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.white), - ), - padding: EdgeInsets.all(20), - ), - ] - ) - ) - ], - ) - ); - } -} - Future scanQrCode(BuildContext context) async { - Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeQRView(BarcodeScanHandler()))); + Navigator.push(context, MaterialPageRoute(builder: (context) => barcodeController(BarcodeScanHandler()))); return; } @@ -829,7 +556,7 @@ SpeedDialChild customBarcodeAction(BuildContext context, RefreshableState state, Navigator.push( context, MaterialPageRoute( - builder: (context) => InvenTreeQRView(handler) + builder: (context) => barcodeController(handler) ) ); } diff --git a/lib/barcode/camera_controller.dart b/lib/barcode/camera_controller.dart new file mode 100644 index 00000000..d7147ce7 --- /dev/null +++ b/lib/barcode/camera_controller.dart @@ -0,0 +1,154 @@ +import "dart:io"; +import "package:flutter/material.dart"; + +import "package:qr_code_scanner/qr_code_scanner.dart"; + +import "package:inventree/l10.dart"; + +import "package:inventree/barcode/handler.dart"; +import "package:inventree/barcode/controller.dart"; + +/* + * Barcode controller which uses the device's camera to scan barcodes. + * Under the hood it uses the qr_code_scanner package. + */ +class CameraBarcodeController extends InvenTreeBarcodeController { + + const CameraBarcodeController(BarcodeHandler handler, {Key? key}) : super(handler, key: key); + + @override + State createState() => _CameraBarcodeControllerState(); + +} + + +class _CameraBarcodeControllerState extends InvenTreeBarcodeControllerState { + + _CameraBarcodeControllerState() : super(); + + QRViewController? _controller; + + bool flash_status = false; + + /* Callback function when the Barcode scanner view is initially created */ + void _onViewCreated(BuildContext context, QRViewController controller) { + _controller = controller; + + controller.scannedDataStream.listen((barcode) { + handleBarcodeData(barcode.code); + }); + } + + // In order to get hot reload to work we need to pause the camera if the platform + // is android, or resume the camera if the platform is iOS. + @override + void reassemble() { + super.reassemble(); + + if (mounted) { + if (Platform.isAndroid) { + _controller!.pauseCamera(); + } + + _controller!.resumeCamera(); + } + } + + @override + void dispose() { + _controller?.dispose(); + super.dispose(); + } + + @override + Future pauseScan() async { + try { + await _controller?.pauseCamera(); + } on CameraException { + // do nothing + } + } + + @override + Future resumeScan() async { + try { + await _controller?.resumeCamera(); + } on CameraException { + // do nothing + } + } + + // Toggle the status of the camera flash + Future updateFlashStatus() async { + final bool? status = await _controller?.getFlashStatus(); + + if (mounted) { + setState(() { + flash_status = status != null && status; + }); + } + } + + @override + Widget build(BuildContext context) { + + return Scaffold( + appBar: AppBar( + title: Text(L10().scanBarcode), + actions: [ + IconButton( + icon: Icon(Icons.flip_camera_android), + onPressed: () { + _controller?.flipCamera(); + } + ), + IconButton( + icon: flash_status ? Icon(Icons.flash_off) : Icon(Icons.flash_on), + onPressed: () { + _controller?.toggleFlash(); + updateFlashStatus(); + }, + ) + ], + ), + body: Stack( + children: [ + Column( + children: [ + Expanded( + child: QRView( + key: barcodeControllerKey, + onQRViewCreated: (QRViewController controller) { + _onViewCreated(context, controller); + }, + overlay: QrScannerOverlayShape( + borderColor: Colors.red, + borderRadius: 10, + borderLength: 30, + borderWidth: 10, + cutOutSize: 300, + ), + ) + ) + ] + ), + Center( + child: Column( + children: [ + Spacer(), + Padding( + child: Text(widget.handler.getOverlayText(context), + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.white), + ), + padding: EdgeInsets.all(20), + ), + ] + ) + ) + ], + ) + ); + } +} diff --git a/lib/barcode/controller.dart b/lib/barcode/controller.dart new file mode 100644 index 00000000..ca8e3966 --- /dev/null +++ b/lib/barcode/controller.dart @@ -0,0 +1,108 @@ +import "package:flutter/material.dart"; +import "package:one_context/one_context.dart"; + +import "package:inventree/preferences.dart"; + +import "package:inventree/barcode/handler.dart"; + +import "package:inventree/widget/progress.dart"; + +/* + * Generic class which provides a barcode scanner interface. + * + * When the controller is instantiated, it is passed a "handler" class, + * which is used to process the scanned barcode. + */ +class InvenTreeBarcodeController extends StatefulWidget { + + const InvenTreeBarcodeController(this.handler, {Key? key}) : super(key: key); + + final BarcodeHandler handler; + + @override + State createState() => InvenTreeBarcodeControllerState(); +} + + +/* + * Base state widget for the barcode controller. + * This defines the basic interface for the barcode controller. + */ +class InvenTreeBarcodeControllerState extends State { + + InvenTreeBarcodeControllerState() : super(); + + final GlobalKey barcodeControllerKey = GlobalKey(debugLabel: "barcodeController"); + + // Internal state flag to test if we are currently processing a barcode + bool processingBarcode = false; + + /* + * Method to handle scanned data. + * Any implementing class should call this method when a barcode is scanned. + * Barcode data should be passed as a string + */ + Future handleBarcodeData(String? data) async { + + // Check that the data is valid, and this view is still mounted + if (!mounted || data == null || data.isEmpty) { + return; + } + + // Currently processing a barcode - ignore this one + if (processingBarcode) { + return; + } + + setState(() { + processingBarcode = true; + }); + + BuildContext? context = OneContext().context; + + showLoadingOverlay(context!); + await pauseScan(); + + await widget.handler.processBarcode(data); + + // processBarcode may have popped the context + if (!mounted) { + hideLoadingOverlay(); + return; + } + + int delay = await InvenTreeSettingsManager().getValue(INV_BARCODE_SCAN_DELAY, 500) as int; + + Future.delayed(Duration(milliseconds: delay), () { + hideLoadingOverlay(); + if (mounted) { + resumeScan().then((_) { + if (mounted) { + setState(() { + processingBarcode = false; + }); + } + }); + } + }); + } + + // Hook function to "pause" the barcode scanner + Future pauseScan() async { + // Implement this function in subclass + } + + // Hook function to "resume" the barcode scanner + Future resumeScan() async { + // Implement this function in subclass + } + + /* + * Implementing classes are in control of building out the widget + */ + @override + Widget build(BuildContext context) { + return Container(); + } + +} \ No newline at end of file diff --git a/lib/barcode/handler.dart b/lib/barcode/handler.dart new file mode 100644 index 00000000..3d72e9c9 --- /dev/null +++ b/lib/barcode/handler.dart @@ -0,0 +1,121 @@ + +import "package:flutter/material.dart"; + +import "package:font_awesome_flutter/font_awesome_flutter.dart"; + +import "package:inventree/api.dart"; +import "package:inventree/helpers.dart"; +import "package:inventree/l10.dart"; + +import "package:inventree/barcode/tones.dart"; + +import "package:inventree/inventree/sentry.dart"; + +import "package:inventree/widget/dialogs.dart"; +import "package:inventree/widget/snacks.dart"; + + +/* Generic class which "handles" a barcode, by communicating with the InvenTree server, + * and handling match / unknown / error cases. + * + * Override functionality of this class to perform custom actions, + * based on the response returned from the InvenTree server + */ +class BarcodeHandler { + + BarcodeHandler(); + + // Return the text to display on the barcode overlay + // Note: Will be overridden by child classes + String getOverlayText(BuildContext context) => "Barcode Overlay"; + + // Called when the server "matches" a barcode + Future onBarcodeMatched(Map data) async { + // Override this function + } + + // Called when the server does not know about a barcode + Future onBarcodeUnknown(Map data) async { + // Override this function + + barcodeFailureTone(); + + showSnackIcon( + L10().barcodeNoMatch, + success: false, + icon: Icons.qr_code, + ); + } + + // Called when the server returns an unhandled response + Future onBarcodeUnhandled(Map data) async { + barcodeFailureTone(); + showServerError("barcode/", L10().responseUnknown, data.toString()); + } + + /* + * Base function to capture and process barcode data. + * + * Returns true only if the barcode scanner should remain open + */ + Future processBarcode(String barcode, {String url = "barcode/"}) async { + + debug("Scanned barcode data: '${barcode}'"); + + barcode = barcode.trim(); + + // Empty barcode is invalid + if (barcode.isEmpty) { + + barcodeFailureTone(); + + showSnackIcon( + L10().barcodeError, + icon: FontAwesomeIcons.circleExclamation, + success: false + ); + + return; + } + + var response = await InvenTreeAPI().post( + url, + body: { + "barcode": barcode, + }, + expectedStatusCode: null, // Do not show an error on "unexpected code" + ); + + debug("Barcode scan response" + response.data.toString()); + + Map data = response.asMap(); + + // Handle strange response from the server + if (!response.isValid() || !response.isMap()) { + onBarcodeUnknown({}); + + showSnackIcon(L10().serverError, success: false); + + // We want to know about this one! + await sentryReportMessage( + "BarcodeHandler.processBarcode returned unexpected value", + context: { + "data": response.data?.toString() ?? "null", + "barcode": barcode, + "url": url, + "statusCode": response.statusCode.toString(), + "valid": response.isValid().toString(), + "error": response.error, + "errorDetail": response.errorDetail, + "className": "${this}", + } + ); + } else if (data.containsKey("success")) { + await onBarcodeMatched(data); + } else if ((response.statusCode >= 400) || data.containsKey("error")) { + await onBarcodeUnknown(data); + } else { + await onBarcodeUnhandled(data); + } + } +} diff --git a/lib/barcode/tones.dart b/lib/barcode/tones.dart new file mode 100644 index 00000000..a6e4b775 --- /dev/null +++ b/lib/barcode/tones.dart @@ -0,0 +1,23 @@ +import "package:inventree/helpers.dart"; +import "package:inventree/preferences.dart"; + +/* + * Play an audible 'success' alert to the user. + */ +Future barcodeSuccessTone() async { + + final bool en = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool; + + if (en) { + playAudioFile("sounds/barcode_scan.mp3"); + } +} + +Future barcodeFailureTone() async { + + final bool en = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool; + + if (en) { + playAudioFile("sounds/barcode_error.mp3"); + } +} \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index dcb9c02c..7116851a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -79,6 +79,12 @@ "availableStock": "Available Stock", "@availableStock": {}, + "barcodes": "Barcodes", + "@barcodes": {}, + + "barcodeSettings": "Barcode Settings", + "@barcodeSettings": {}, + "barcodeAssign": "Assign Barcode", "@barcodeAssign": {}, @@ -106,6 +112,12 @@ "barcodeScanAssign": "Scan to assign barcode", "@barcodeScanAssign": {}, + "barcodeScanDelay": "Barcode Scan Delay", + "@barcodeScanDelay": {}, + + "barcodeScanDelayDetail": "Delay between barcode scans", + "@barcodeScanDelayDetail": {}, + "barcodeScanGeneral": "Scan an InvenTree barcode", "@barcodeScanGeneral": {}, diff --git a/lib/preferences.dart b/lib/preferences.dart index fc08450b..23404dfc 100644 --- a/lib/preferences.dart +++ b/lib/preferences.dart @@ -29,6 +29,9 @@ const String INV_STOCK_SHOW_TESTS = "stockShowTests"; const String INV_REPORT_ERRORS = "reportErrors"; const String INV_STRICT_HTTPS = "strictHttps"; +// Barcode settings +const String INV_BARCODE_SCAN_DELAY = "barcodeScanDelay"; + /* * Class for storing InvenTree preferences in a NoSql DB */ diff --git a/lib/settings/barcode_settings.dart b/lib/settings/barcode_settings.dart new file mode 100644 index 00000000..1624a10f --- /dev/null +++ b/lib/settings/barcode_settings.dart @@ -0,0 +1,114 @@ +import "package:flutter/material.dart"; +import "package:font_awesome_flutter/font_awesome_flutter.dart"; +import "package:inventree/l10.dart"; +import "package:inventree/preferences.dart"; + + +class InvenTreeBarcodeSettingsWidget extends StatefulWidget { + @override + _InvenTreeBarcodeSettingsState createState() => _InvenTreeBarcodeSettingsState(); +} + + +class _InvenTreeBarcodeSettingsState extends State { + + _InvenTreeBarcodeSettingsState(); + + int barcodeScanDelay = 500; + + final TextEditingController _barcodeScanDelayController = TextEditingController(); + + @override + void initState() { + super.initState(); + loadSettings(); + } + + Future loadSettings() async { + barcodeScanDelay = await InvenTreeSettingsManager().getValue(INV_BARCODE_SCAN_DELAY, 500) as int; + + if (mounted) { + setState(() { + }); + } + } + + // Callback function to edit the barcode scan delay value + // TODO: Next time any new settings are added, refactor this into a generic function + Future _editBarcodeScanDelay(BuildContext context) async { + + _barcodeScanDelayController.text = barcodeScanDelay.toString(); + + return showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text(L10().barcodeScanDelay), + content: TextField( + onChanged: (value) {}, + controller: _barcodeScanDelayController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: L10().barcodeScanDelayDetail, + ), + ), + actions: [ + MaterialButton( + color: Colors.red, + textColor: Colors.white, + child: Text(L10().cancel), + onPressed: () { + setState(() { + Navigator.pop(context); + }); + }, + ), + MaterialButton( + color: Colors.green, + textColor: Colors.white, + child: Text(L10().ok), + onPressed: () async { + int delay = int.tryParse(_barcodeScanDelayController.text) ?? barcodeScanDelay; + + // Apply limits + if (delay < 100) delay = 100; + if (delay > 2500) delay = 2500; + + InvenTreeSettingsManager().setValue(INV_BARCODE_SCAN_DELAY, delay); + setState(() { + barcodeScanDelay = delay; + Navigator.pop(context); + }); + }, + ), + ], + ); + } + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(L10().barcodes)), + body: Container( + child: ListView( + children: [ + ListTile( + title: Text(L10().barcodeScanDelay), + subtitle: Text(L10().barcodeScanDelayDetail), + leading: FaIcon(FontAwesomeIcons.stopwatch), + trailing: GestureDetector( + child: Text("${barcodeScanDelay} ms"), + onTap: () { + _editBarcodeScanDelay(context); + }, + ), + ) + ], + ) + ) + ); + } + +} \ No newline at end of file diff --git a/lib/settings/settings.dart b/lib/settings/settings.dart index b202e6de..f5b8e2d5 100644 --- a/lib/settings/settings.dart +++ b/lib/settings/settings.dart @@ -4,8 +4,10 @@ import "package:package_info_plus/package_info_plus.dart"; import "package:inventree/app_colors.dart"; import "package:inventree/l10.dart"; + import "package:inventree/settings/about.dart"; import "package:inventree/settings/app_settings.dart"; +import "package:inventree/settings/barcode_settings.dart"; import "package:inventree/settings/home_settings.dart"; import "package:inventree/settings/login.dart"; import "package:inventree/settings/part_settings.dart"; @@ -68,6 +70,14 @@ class _InvenTreeSettingsState extends State { Navigator.push(context, MaterialPageRoute(builder: (context) => HomeScreenSettingsWidget())); } ), + ListTile( + title: Text(L10().barcodes), + subtitle: Text(L10().barcodeSettings), + leading: FaIcon(FontAwesomeIcons.barcode, color: COLOR_ACTION), + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeBarcodeSettingsWidget())); + } + ), ListTile( title: Text(L10().part), subtitle: Text(L10().partSettings), diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart index d693314b..bd43ab4e 100644 --- a/lib/widget/location_display.dart +++ b/lib/widget/location_display.dart @@ -4,7 +4,7 @@ import "package:flutter_speed_dial/flutter_speed_dial.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:inventree/app_colors.dart"; -import "package:inventree/barcode.dart"; +import "package:inventree/barcode/barcode.dart"; import "package:inventree/l10.dart"; import "package:inventree/inventree/stock.dart"; @@ -94,7 +94,7 @@ class _LocationDisplayState extends RefreshableState { Navigator.push( context, MaterialPageRoute(builder: (context) => - InvenTreeQRView( + barcodeController( StockLocationScanInItemsHandler(location!))) ).then((value) { refresh(context); @@ -114,8 +114,8 @@ class _LocationDisplayState extends RefreshableState { Navigator.push( context, MaterialPageRoute(builder: (context) => - InvenTreeQRView( - ScanParentLocationHandler(location!))) + barcodeController(ScanParentLocationHandler(location!)) + ) ).then((value) { refresh(context); }); diff --git a/lib/widget/part_detail.dart b/lib/widget/part_detail.dart index bcfb733c..1932780f 100644 --- a/lib/widget/part_detail.dart +++ b/lib/widget/part_detail.dart @@ -4,7 +4,7 @@ import "package:flutter_speed_dial/flutter_speed_dial.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:inventree/app_colors.dart"; -import "package:inventree/barcode.dart"; +import "package:inventree/barcode/barcode.dart"; import "package:inventree/l10.dart"; import "package:inventree/helpers.dart"; diff --git a/lib/widget/refreshable_state.dart b/lib/widget/refreshable_state.dart index 69460727..7d2eb348 100644 --- a/lib/widget/refreshable_state.dart +++ b/lib/widget/refreshable_state.dart @@ -3,7 +3,7 @@ import "package:flutter_speed_dial/flutter_speed_dial.dart"; import "package:inventree/api.dart"; import "package:inventree/app_colors.dart"; -import "package:inventree/barcode.dart"; +import "package:inventree/barcode/barcode.dart"; import "package:inventree/widget/back.dart"; import "package:inventree/widget/drawer.dart"; diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index cb656799..37077f19 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -4,7 +4,7 @@ import "package:flutter_speed_dial/flutter_speed_dial.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:inventree/app_colors.dart"; -import "package:inventree/barcode.dart"; +import "package:inventree/barcode/barcode.dart"; import "package:inventree/helpers.dart"; import "package:inventree/l10.dart"; import "package:inventree/api.dart"; @@ -168,8 +168,9 @@ class _StockItemDisplayState extends RefreshableState { Navigator.push( context, MaterialPageRoute(builder: (context) => - InvenTreeQRView( - StockItemScanIntoLocationHandler(widget.item))) + barcodeController( + StockItemScanIntoLocationHandler(widget.item)) + ) ).then((ctx) { refresh(context); }); diff --git a/lib/widget/supplier_part_detail.dart b/lib/widget/supplier_part_detail.dart index 22768d14..9821bfb1 100644 --- a/lib/widget/supplier_part_detail.dart +++ b/lib/widget/supplier_part_detail.dart @@ -4,7 +4,7 @@ import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:inventree/api.dart"; import "package:inventree/app_colors.dart"; -import "package:inventree/barcode.dart"; +import "package:inventree/barcode/barcode.dart"; import "package:inventree/l10.dart"; import "package:inventree/inventree/part.dart"; diff --git a/test/barcode_test.dart b/test/barcode_test.dart index e7b3eee7..e896f440 100644 --- a/test/barcode_test.dart +++ b/test/barcode_test.dart @@ -8,7 +8,7 @@ import "package:flutter_test/flutter_test.dart"; import "package:inventree/api.dart"; -import "package:inventree/barcode.dart"; +import "package:inventree/barcode/barcode.dart"; import "package:inventree/helpers.dart"; import "package:inventree/user_profile.dart"; @@ -57,7 +57,7 @@ void main() { test("Empty Barcode", () async { // Handle an 'empty' barcode - await handler.processBarcode(null, ""); + await handler.processBarcode(""); debugContains("Scanned barcode data: ''"); debugContains("showSnackIcon: 'Barcode scan error'"); @@ -68,7 +68,7 @@ void main() { test("Junk Data", () async { // test scanning 'junk' data - await handler.processBarcode(null, "abcdefg"); + await handler.processBarcode("abcdefg"); debugContains("Scanned barcode data: 'abcdefg'"); debugContains("showSnackIcon: 'No match for barcode'"); @@ -76,7 +76,7 @@ void main() { test("Invalid StockLocation", () async { // Scan an invalid stock location - await handler.processBarcode(null, '{"stocklocation": 999999}'); + await handler.processBarcode('{"stocklocation": 999999}'); debugContains("Scanned barcode data: '{\"stocklocation\": 999999}'"); debugContains("showSnackIcon: 'No match for barcode'"); @@ -97,7 +97,7 @@ void main() { var handler = StockItemScanIntoLocationHandler(item!); - await handler.processBarcode(null, '{"stocklocation": 7}'); + await handler.processBarcode('{"stocklocation": 7}'); // Check the location has been updated await item.reload(); assert(item.locationId == 7); @@ -105,7 +105,7 @@ void main() { debugContains("Scanned stock location 7"); // Scan into a new location - await handler.processBarcode(null, '{"stocklocation": 1}'); + await handler.processBarcode('{"stocklocation": 1}'); await item.reload(); assert(item.locationId == 1); @@ -125,7 +125,7 @@ void main() { // Scan multiple items into this location for (int id in [1, 2, 11]) { - await handler.processBarcode(null, '{"stockitem": ${id}}'); + await handler.processBarcode('{"stockitem": ${id}}'); var item = await InvenTreeStockItem().get(id) as InvenTreeStockItem?; @@ -150,12 +150,12 @@ void main() { var handler = ScanParentLocationHandler(location!); // Scan into new parent location - await handler.processBarcode(null, '{"stocklocation": 1}'); + await handler.processBarcode('{"stocklocation": 1}'); await location.reload(); assert(location.parentId == 1); // Scan back into old parent location - await handler.processBarcode(null, '{"stocklocation": 4}'); + await handler.processBarcode('{"stocklocation": 4}'); await location.reload(); assert(location.parentId == 4);