mirror of
				https://github.com/inventree/inventree-app.git
				synced 2025-10-31 13:25:40 +00:00 
			
		
		
		
	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
This commit is contained in:
		| @@ -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) | ||||
|               ) | ||||
|             ); | ||||
|           }, | ||||
|   | ||||
| @@ -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<void> 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 <void> 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<void> onBarcodeMatched(Map<String, dynamic> data) async { | ||||
|       // Called when the server "matches" a barcode | ||||
|       // Override this function | ||||
|     } | ||||
| 
 | ||||
|     Future<void> onBarcodeUnknown(Map<String, dynamic> 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<void> onBarcodeUnhandled(Map<String, dynamic> 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<void> 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<String, dynamic> 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<StatefulWidget> createState() => _QRViewState(); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| class _QRViewState extends State<InvenTreeQRView> { | ||||
| 
 | ||||
|   _QRViewState() : super(); | ||||
| 
 | ||||
|   final GlobalKey qrKey = GlobalKey(debugLabel: "QR"); | ||||
| 
 | ||||
|   QRViewController? _controller; | ||||
| 
 | ||||
|   bool flash_status = false; | ||||
| 
 | ||||
|   bool currently_processing = false; | ||||
| 
 | ||||
|   Future<void> 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<void> 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: <Widget>[ | ||||
|             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<void> 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) | ||||
|             ) | ||||
|         ); | ||||
|       } | ||||
							
								
								
									
										154
									
								
								lib/barcode/camera_controller.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								lib/barcode/camera_controller.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -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<StatefulWidget> 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<void> pauseScan() async { | ||||
|     try { | ||||
|       await _controller?.pauseCamera(); | ||||
|     } on CameraException { | ||||
|       // do nothing | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<void> resumeScan() async { | ||||
|     try { | ||||
|       await _controller?.resumeCamera(); | ||||
|     } on CameraException { | ||||
|       // do nothing | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Toggle the status of the camera flash | ||||
|   Future<void> 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: <Widget>[ | ||||
|             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), | ||||
|                       ), | ||||
|                     ] | ||||
|                 ) | ||||
|             ) | ||||
|           ], | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										108
									
								
								lib/barcode/controller.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								lib/barcode/controller.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -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<StatefulWidget> createState() => InvenTreeBarcodeControllerState(); | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Base state widget for the barcode controller. | ||||
|  * This defines the basic interface for the barcode controller. | ||||
|  */ | ||||
| class InvenTreeBarcodeControllerState extends State<InvenTreeBarcodeController> { | ||||
|  | ||||
|   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<void> 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<void> pauseScan() async { | ||||
|     // Implement this function in subclass | ||||
|   } | ||||
|  | ||||
|   // Hook function to "resume" the barcode scanner | ||||
|   Future<void> resumeScan() async { | ||||
|     // Implement this function in subclass | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|    * Implementing classes are in control of building out the widget | ||||
|    */ | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Container(); | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										121
									
								
								lib/barcode/handler.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								lib/barcode/handler.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -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<void> onBarcodeMatched(Map<String, dynamic> data) async { | ||||
|     // Override this function | ||||
|   } | ||||
|  | ||||
|   // Called when the server does not know about a barcode | ||||
|   Future<void> onBarcodeUnknown(Map<String, dynamic> data) async { | ||||
|     // Override this function | ||||
|  | ||||
|     barcodeFailureTone(); | ||||
|  | ||||
|     showSnackIcon( | ||||
|       L10().barcodeNoMatch, | ||||
|       success: false, | ||||
|       icon: Icons.qr_code, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   // Called when the server returns an unhandled response | ||||
|   Future<void> onBarcodeUnhandled(Map<String, dynamic> 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<void> 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<String, dynamic> 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); | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										23
									
								
								lib/barcode/tones.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								lib/barcode/tones.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| import "package:inventree/helpers.dart"; | ||||
| import "package:inventree/preferences.dart"; | ||||
|  | ||||
| /* | ||||
|  * Play an audible 'success' alert to the user. | ||||
|  */ | ||||
| Future<void> barcodeSuccessTone() async { | ||||
|  | ||||
|   final bool en = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool; | ||||
|  | ||||
|   if (en) { | ||||
|     playAudioFile("sounds/barcode_scan.mp3"); | ||||
|   } | ||||
| } | ||||
|  | ||||
| Future <void> barcodeFailureTone() async { | ||||
|  | ||||
|   final bool en = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool; | ||||
|  | ||||
|   if (en) { | ||||
|     playAudioFile("sounds/barcode_error.mp3"); | ||||
|   } | ||||
| } | ||||
| @@ -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": {}, | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|  */ | ||||
|   | ||||
							
								
								
									
										114
									
								
								lib/settings/barcode_settings.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								lib/settings/barcode_settings.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -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<InvenTreeBarcodeSettingsWidget> { | ||||
|  | ||||
|  _InvenTreeBarcodeSettingsState(); | ||||
|  | ||||
|  int barcodeScanDelay = 500; | ||||
|  | ||||
|  final TextEditingController _barcodeScanDelayController = TextEditingController(); | ||||
|  | ||||
|  @override | ||||
|  void initState() { | ||||
|   super.initState(); | ||||
|   loadSettings(); | ||||
|  } | ||||
|  | ||||
|   Future<void> 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<void> _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: <Widget>[ | ||||
|             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); | ||||
|                 }, | ||||
|               ), | ||||
|             ) | ||||
|           ], | ||||
|         ) | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -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<InvenTreeSettingsWidget> { | ||||
|                   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), | ||||
|   | ||||
| @@ -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<LocationDisplayWidget> { | ||||
|                   Navigator.push( | ||||
|                       context, | ||||
|                       MaterialPageRoute(builder: (context) => | ||||
|                           InvenTreeQRView( | ||||
|                           barcodeController( | ||||
|                               StockLocationScanInItemsHandler(location!))) | ||||
|                   ).then((value) { | ||||
|                     refresh(context); | ||||
| @@ -114,8 +114,8 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> { | ||||
|                   Navigator.push( | ||||
|                       context, | ||||
|                       MaterialPageRoute(builder: (context) => | ||||
|                           InvenTreeQRView( | ||||
|                               ScanParentLocationHandler(location!))) | ||||
|                         barcodeController(ScanParentLocationHandler(location!)) | ||||
|                       ) | ||||
|                   ).then((value) { | ||||
|                     refresh(context); | ||||
|                   }); | ||||
|   | ||||
| @@ -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"; | ||||
|  | ||||
|   | ||||
| @@ -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"; | ||||
|   | ||||
| @@ -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<StockDetailWidget> { | ||||
|                 Navigator.push( | ||||
|                     context, | ||||
|                     MaterialPageRoute(builder: (context) => | ||||
|                         InvenTreeQRView( | ||||
|                             StockItemScanIntoLocationHandler(widget.item))) | ||||
|                       barcodeController( | ||||
|                         StockItemScanIntoLocationHandler(widget.item)) | ||||
|                       ) | ||||
|                 ).then((ctx) { | ||||
|                   refresh(context); | ||||
|                 }); | ||||
|   | ||||
| @@ -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"; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user