mirror of
				https://github.com/inventree/inventree-app.git
				synced 2025-10-31 05:15:42 +00:00 
			
		
		
		
	Scanner wedge mode (#437)
* Add code_scan_listener package * Implement wedge controller widget * Update barcode settings widget - Allow user to choose which barcode scanner to use * Fix typo * Select barcode scanner widget based on user preference * Fix rendering issues for wedge controller * Update release notes * Add unit test for wedge scanner widget - Required some tweaks to other code * Use better library - https://github.com/fuadreza/flutter_barcode_listener - Fork of https://github.com/shaxxx/flutter_barcode_listener - Properly handles key "case" issues (shift, essentially) - Verified that it works correctly for multiple character types * Local copy of code, rather than relying on package which is not available on pub.dev * Fix unit test
This commit is contained in:
		| @@ -2,6 +2,7 @@ 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/preferences.dart"; | ||||
| import "package:one_context/one_context.dart"; | ||||
|  | ||||
|  | ||||
| @@ -11,6 +12,7 @@ import "package:inventree/helpers.dart"; | ||||
| import "package:inventree/l10.dart"; | ||||
|  | ||||
| import "package:inventree/barcode/camera_controller.dart"; | ||||
| import "package:inventree/barcode/wedge_controller.dart"; | ||||
| import "package:inventree/barcode/controller.dart"; | ||||
| import "package:inventree/barcode/handler.dart"; | ||||
| import "package:inventree/barcode/tones.dart"; | ||||
| @@ -44,6 +46,19 @@ Future<Object?> scanBarcode(BuildContext context, {BarcodeHandler? handler}) asy | ||||
|    | ||||
|   InvenTreeBarcodeController controller = CameraBarcodeController(handler); | ||||
|  | ||||
|   // Select barcode controller based on user preference | ||||
|   final int barcodeControllerType = await InvenTreeSettingsManager().getValue(INV_BARCODE_SCAN_TYPE, BARCODE_CONTROLLER_CAMERA) as int; | ||||
|  | ||||
|   switch (barcodeControllerType) { | ||||
|     case BARCODE_CONTROLLER_WEDGE: | ||||
|       controller = WedgeBarcodeController(handler); | ||||
|       break; | ||||
|     case BARCODE_CONTROLLER_CAMERA: | ||||
|     default: | ||||
|       // Already set as default option | ||||
|       break; | ||||
|   } | ||||
|  | ||||
|   return Navigator.of(context).push( | ||||
|     PageRouteBuilder( | ||||
|       pageBuilder: (context, _, __) => controller, | ||||
|   | ||||
| @@ -58,9 +58,9 @@ class InvenTreeBarcodeControllerState extends State<InvenTreeBarcodeController> | ||||
|       processingBarcode = true; | ||||
|     }); | ||||
|  | ||||
|     BuildContext? context = OneContext().context; | ||||
|     BuildContext? context = OneContext.hasContext ? OneContext().context : null; | ||||
|  | ||||
|     showLoadingOverlay(context!); | ||||
|     showLoadingOverlay(context); | ||||
|     await pauseScan(); | ||||
|  | ||||
|     await widget.handler.processBarcode(data); | ||||
|   | ||||
							
								
								
									
										175
									
								
								lib/barcode/flutter_barcode_listener.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								lib/barcode/flutter_barcode_listener.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,175 @@ | ||||
|  | ||||
| /* | ||||
|  * Custom keyboard listener which allows the app to act as a keyboard "wedge", | ||||
|  * and intercept barcodes from any compatible scanner. | ||||
|  * | ||||
|  * Note: This code was copied from https://github.com/fuadreza/flutter_barcode_listener/blob/master/lib/flutter_barcode_listener.dart | ||||
|  * | ||||
|  * If that code becomes available on pub.dev, we can remove this file and reference that library | ||||
|  */ | ||||
|  | ||||
| import "dart:async"; | ||||
|  | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:flutter/services.dart"; | ||||
|  | ||||
| typedef BarcodeScannedCallback = void Function(String barcode); | ||||
|  | ||||
| /// This widget will listen for raw PHYSICAL keyboard events | ||||
| /// even when other controls have primary focus. | ||||
| /// It will buffer all characters coming in specifed `bufferDuration` time frame | ||||
| /// that end with line feed character and call callback function with result. | ||||
| /// Keep in mind this widget will listen for events even when not visible. | ||||
| /// Windows seems to be using the [RawKeyDownEvent] instead of the | ||||
| /// [RawKeyUpEvent], this behaviour can be managed by setting [useKeyDownEvent]. | ||||
| class BarcodeKeyboardListener extends StatefulWidget { | ||||
|  | ||||
|   /// This widget will listen for raw PHYSICAL keyboard events | ||||
|   /// even when other controls have primary focus. | ||||
|   /// It will buffer all characters coming in specifed `bufferDuration` time frame | ||||
|   /// that end with line feed character and call callback function with result. | ||||
|   /// Keep in mind this widget will listen for events even when not visible. | ||||
|   const BarcodeKeyboardListener( | ||||
|       {Key? key, | ||||
|  | ||||
|         /// Child widget to be displayed. | ||||
|         required this.child, | ||||
|  | ||||
|         /// Callback to be called when barcode is scanned. | ||||
|         required Function(String) onBarcodeScanned, | ||||
|  | ||||
|         /// When experiencing issueswith empty barcodes on Windows, | ||||
|         /// set this value to true. Default value is `false`. | ||||
|         this.useKeyDownEvent = false, | ||||
|  | ||||
|         /// Maximum time between two key events. | ||||
|         /// If time between two key events is longer than this value | ||||
|         /// previous keys will be ignored. | ||||
|         Duration bufferDuration = hundredMs}) | ||||
|       : _onBarcodeScanned = onBarcodeScanned, | ||||
|         _bufferDuration = bufferDuration, | ||||
|         super(key: key); | ||||
|  | ||||
|   final Widget child; | ||||
|   final BarcodeScannedCallback _onBarcodeScanned; | ||||
|   final Duration _bufferDuration; | ||||
|   final bool useKeyDownEvent; | ||||
|  | ||||
|   @override | ||||
|   _BarcodeKeyboardListenerState createState() => _BarcodeKeyboardListenerState( | ||||
|       _onBarcodeScanned, _bufferDuration, useKeyDownEvent); | ||||
| } | ||||
|  | ||||
| const Duration aSecond = Duration(seconds: 1); | ||||
| const Duration hundredMs = Duration(milliseconds: 100); | ||||
| const String lineFeed = "\n"; | ||||
|  | ||||
| class _BarcodeKeyboardListenerState extends State<BarcodeKeyboardListener> { | ||||
|  | ||||
|   _BarcodeKeyboardListenerState(this._onBarcodeScannedCallback, | ||||
|       this._bufferDuration, this._useKeyDownEvent) { | ||||
|     RawKeyboard.instance.addListener(_keyBoardCallback); | ||||
|     _keyboardSubscription = | ||||
|         _controller.stream.where((char) => char != null).listen(onKeyEvent); | ||||
|   } | ||||
|  | ||||
|   List<String> _scannedChars = []; | ||||
|   DateTime? _lastScannedCharCodeTime; | ||||
|   late StreamSubscription<String?> _keyboardSubscription; | ||||
|  | ||||
|   final BarcodeScannedCallback _onBarcodeScannedCallback; | ||||
|   final Duration _bufferDuration; | ||||
|  | ||||
|   final _controller = StreamController<String?>(); | ||||
|  | ||||
|   final bool _useKeyDownEvent; | ||||
|  | ||||
|   bool _isShiftPressed = false; | ||||
|   void onKeyEvent(String? char) { | ||||
|     //remove any pending characters older than bufferDuration value | ||||
|     checkPendingCharCodesToClear(); | ||||
|     _lastScannedCharCodeTime = DateTime.now(); | ||||
|     if (char == lineFeed) { | ||||
|       _onBarcodeScannedCallback.call(_scannedChars.join()); | ||||
|       resetScannedCharCodes(); | ||||
|     } else { | ||||
|       //add character to list of scanned characters; | ||||
|       _scannedChars.add(char!); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void checkPendingCharCodesToClear() { | ||||
|     if (_lastScannedCharCodeTime != null) { | ||||
|       if (_lastScannedCharCodeTime! | ||||
|           .isBefore(DateTime.now().subtract(_bufferDuration))) { | ||||
|         resetScannedCharCodes(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void resetScannedCharCodes() { | ||||
|     _lastScannedCharCodeTime = null; | ||||
|     _scannedChars = []; | ||||
|   } | ||||
|  | ||||
|   void addScannedCharCode(String charCode) { | ||||
|     _scannedChars.add(charCode); | ||||
|   } | ||||
|  | ||||
|   void _keyBoardCallback(RawKeyEvent keyEvent) { | ||||
|     if (keyEvent.logicalKey.keyId > 255 && | ||||
|         keyEvent.data.logicalKey != LogicalKeyboardKey.enter && | ||||
|         keyEvent.data.logicalKey != LogicalKeyboardKey.shiftLeft) return; | ||||
|     if ((!_useKeyDownEvent && keyEvent is RawKeyUpEvent) || | ||||
|         (_useKeyDownEvent && keyEvent is RawKeyDownEvent)) { | ||||
|       if (keyEvent.data is RawKeyEventDataAndroid) { | ||||
|         if (keyEvent.data.logicalKey == LogicalKeyboardKey.shiftLeft) { | ||||
|           _isShiftPressed = true; | ||||
|         } else { | ||||
|           if (_isShiftPressed) { | ||||
|             _isShiftPressed = false; | ||||
|             _controller.sink.add(String.fromCharCode( | ||||
|                 ((keyEvent.data) as RawKeyEventDataAndroid).codePoint).toUpperCase()); | ||||
|           } else { | ||||
|             _controller.sink.add(String.fromCharCode( | ||||
|                 ((keyEvent.data) as RawKeyEventDataAndroid).codePoint)); | ||||
|           } | ||||
|         } | ||||
|       } else if (keyEvent.data is RawKeyEventDataFuchsia) { | ||||
|         _controller.sink.add(String.fromCharCode( | ||||
|             ((keyEvent.data) as RawKeyEventDataFuchsia).codePoint)); | ||||
|       } else if (keyEvent.data.logicalKey == LogicalKeyboardKey.enter) { | ||||
|         _controller.sink.add(lineFeed); | ||||
|       } else if (keyEvent.data is RawKeyEventDataWeb) { | ||||
|         _controller.sink.add(((keyEvent.data) as RawKeyEventDataWeb).keyLabel); | ||||
|       } else if (keyEvent.data is RawKeyEventDataLinux) { | ||||
|         _controller.sink | ||||
|             .add(((keyEvent.data) as RawKeyEventDataLinux).keyLabel); | ||||
|       } else if (keyEvent.data is RawKeyEventDataWindows) { | ||||
|         _controller.sink.add(String.fromCharCode( | ||||
|             ((keyEvent.data) as RawKeyEventDataWindows).keyCode)); | ||||
|       } else if (keyEvent.data is RawKeyEventDataMacOs) { | ||||
|         _controller.sink | ||||
|             .add(((keyEvent.data) as RawKeyEventDataMacOs).characters); | ||||
|       } else if (keyEvent.data is RawKeyEventDataIos) { | ||||
|         _controller.sink | ||||
|             .add(((keyEvent.data) as RawKeyEventDataIos).characters); | ||||
|       } else { | ||||
|         _controller.sink.add(keyEvent.character); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return widget.child; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _keyboardSubscription.cancel(); | ||||
|     _controller.close(); | ||||
|     RawKeyboard.instance.removeListener(_keyBoardCallback); | ||||
|     super.dispose(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										102
									
								
								lib/barcode/wedge_controller.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								lib/barcode/wedge_controller.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
|  | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
|  | ||||
| import "package:inventree/app_colors.dart"; | ||||
| import "package:inventree/barcode/controller.dart"; | ||||
| import "package:inventree/barcode/handler.dart"; | ||||
| import "package:inventree/barcode/flutter_barcode_listener.dart"; | ||||
| import "package:inventree/l10.dart"; | ||||
| import "package:inventree/helpers.dart"; | ||||
|  | ||||
| /* | ||||
|  * Barcode controller which acts as a keyboard wedge, | ||||
|  * intercepting barcode data which is entered as rapid keyboard presses | ||||
|  */ | ||||
| class WedgeBarcodeController extends InvenTreeBarcodeController { | ||||
|  | ||||
|   const WedgeBarcodeController(BarcodeHandler handler, {Key? key}) : super(handler, key: key); | ||||
|  | ||||
|   @override | ||||
|   State<StatefulWidget> createState() => _WedgeBarcodeControllerState(); | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| class _WedgeBarcodeControllerState extends InvenTreeBarcodeControllerState { | ||||
|  | ||||
|   _WedgeBarcodeControllerState() : super(); | ||||
|  | ||||
|   bool canScan = true; | ||||
|  | ||||
|   bool get scanning => mounted && canScan; | ||||
|  | ||||
|   @override | ||||
|   Future<void> pauseScan() async { | ||||
|  | ||||
|     if (mounted) { | ||||
|       setState(() { | ||||
|         canScan = false; | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<void> resumeScan() async { | ||||
|  | ||||
|     if (mounted) { | ||||
|       setState(() { | ||||
|         canScan = true; | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|  | ||||
|     return Scaffold( | ||||
|       appBar: AppBar( | ||||
|         title: Text(L10().scanBarcode), | ||||
|       ), | ||||
|       backgroundColor: Colors.black.withOpacity(0.9), | ||||
|       body: Center( | ||||
|         child: Column( | ||||
|           mainAxisAlignment: MainAxisAlignment.center, | ||||
|           children: [ | ||||
|             Spacer(flex: 5), | ||||
|             FaIcon(FontAwesomeIcons.barcode, size: 64), | ||||
|             Spacer(flex: 5), | ||||
|             BarcodeKeyboardListener( | ||||
|               useKeyDownEvent: true, | ||||
|               child: SizedBox( | ||||
|                 child: CircularProgressIndicator( | ||||
|                   color: scanning ? COLOR_ACTION : COLOR_PROGRESS | ||||
|                 ), | ||||
|                 width: 64, | ||||
|                 height: 64, | ||||
|               ), | ||||
|               onBarcodeScanned: (String barcode) { | ||||
|                 debug("scanned: ${barcode}"); | ||||
|                 if (scanning) { | ||||
|                   // Process the barcode data | ||||
|                   handleBarcodeData(barcode); | ||||
|                 } | ||||
|               }, | ||||
|             ), | ||||
|             Spacer(flex: 5), | ||||
|             Padding( | ||||
|               child: Text( | ||||
|                 widget.handler.getOverlayText(context), | ||||
|                 style: TextStyle( | ||||
|                   fontWeight: FontWeight.bold, | ||||
|                   color: Colors.white) | ||||
|               ), | ||||
|               padding: EdgeInsets.all(20), | ||||
|             ) | ||||
|           ], | ||||
|         ) | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
		Reference in New Issue
	
	Block a user