mirror of
				https://github.com/inventree/inventree-app.git
				synced 2025-10-31 13:25:40 +00:00 
			
		
		
		
	Barcode scanner updates (#562)
* Add BUILDING.md * Replace scaning library - Out with qr_code_scanner - In with flutter_zxing * Update specs for jdk / kotlin / gradle - NFI what this all means? * Refactor barcode scanning widget * Refactor barcode overlay * Add handlers * Update release notes * Fix AppBar color * Enhance attachment widget * remove unused import * Improved icon * Select theme from main drawer
This commit is contained in:
		| @@ -1,10 +1,10 @@ | ||||
| import "dart:io"; | ||||
| import "dart:math"; | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:flutter_tabler_icons/flutter_tabler_icons.dart"; | ||||
| import "package:inventree/app_colors.dart"; | ||||
| import "package:inventree/preferences.dart"; | ||||
|  | ||||
| import "package:qr_code_scanner/qr_code_scanner.dart"; | ||||
| import "package:flutter_zxing/flutter_zxing.dart"; | ||||
|  | ||||
| import "package:inventree/l10.dart"; | ||||
|  | ||||
| @@ -26,197 +26,234 @@ class CameraBarcodeController extends InvenTreeBarcodeController { | ||||
| class _CameraBarcodeControllerState extends InvenTreeBarcodeControllerState { | ||||
|   _CameraBarcodeControllerState() : super(); | ||||
|  | ||||
|   QRViewController? _controller; | ||||
|  | ||||
|   bool flash_status = false; | ||||
|  | ||||
|   int scan_delay = 500; | ||||
|   bool single_scanning = false; | ||||
|   bool scanning_paused = false; | ||||
|  | ||||
|   String scanned_code = ""; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _loadSettings(); | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|    * Load the barcode scanning settings | ||||
|    */ | ||||
|   Future<void> _loadSettings() async { | ||||
|     bool _single = await InvenTreeSettingsManager() | ||||
|         .getBool(INV_BARCODE_SCAN_SINGLE, false); | ||||
|  | ||||
|     int _delay = await InvenTreeSettingsManager() | ||||
|         .getValue(INV_BARCODE_SCAN_DELAY, 500) as int; | ||||
|  | ||||
|     if (mounted) { | ||||
|       setState(() { | ||||
|         scan_delay = _delay; | ||||
|         single_scanning = _single; | ||||
|         scanning_paused = false; | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /* Callback function when the Barcode scanner view is initially created */ | ||||
|   void _onViewCreated(BuildContext context, QRViewController controller) { | ||||
|     _controller = controller; | ||||
|  | ||||
|     controller.scannedDataStream.listen((barcode) { | ||||
|       if (!scanning_paused) { | ||||
|         handleBarcodeData(barcode.code).then((value) => { | ||||
|               // If in single-scanning mode, pause after successful scan | ||||
|               if (single_scanning && mounted) | ||||
|                 { | ||||
|                   setState(() { | ||||
|                     scanning_paused = true; | ||||
|                   }) | ||||
|                 } | ||||
|             }); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     _loadSettings(); | ||||
|   } | ||||
|  | ||||
|   // 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 { | ||||
|     // Do not attempt to resume if the widget is not mounted | ||||
|     if (!mounted) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     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; | ||||
|         scanning_paused = true; | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     Widget actionIcon = | ||||
|         Icon(TablerIcons.player_pause, color: COLOR_WARNING, size: 64); | ||||
|   Future<void> resumeScan() async { | ||||
|     if (mounted) { | ||||
|       setState(() { | ||||
|         scanning_paused = false; | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|    * Callback function when a barcode is scanned | ||||
|    */ | ||||
|   void _onScanSuccess(Code? code) { | ||||
|  | ||||
|     if (scanning_paused) { | ||||
|       actionIcon = | ||||
|           Icon(TablerIcons.player_play, color: COLOR_ACTION, size: 64); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     String barcode_data = code?.text ?? ""; | ||||
|  | ||||
|     if (mounted) { | ||||
|       setState(() { | ||||
|         scanned_code = barcode_data; | ||||
|         scanning_paused = barcode_data.isNotEmpty; | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     if (barcode_data.isNotEmpty) { | ||||
|       handleBarcodeData(barcode_data).then((_) { | ||||
|         if (!single_scanning && mounted) { | ||||
|           // Resume next scan | ||||
|           setState(() { | ||||
|             scanning_paused = false; | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|    * Build the barcode scanner overlay | ||||
|    */ | ||||
|   FixedScannerOverlay BarcodeOverlay(BuildContext context) { | ||||
|  | ||||
|     // Note: Copied from reader_widget.dart:ReaderWidget.build | ||||
|     final Size size = MediaQuery.of(context).size; | ||||
|     final double cropSize = min(size.width, size.height) * 0.5; | ||||
|  | ||||
|     return FixedScannerOverlay( | ||||
|       borderColor: scanning_paused ? COLOR_WARNING : COLOR_ACTION, | ||||
|       overlayColor: Colors.black45, | ||||
|       borderRadius: 1, | ||||
|       borderLength: 15, | ||||
|       borderWidth: 8, | ||||
|       cutOutSize: cropSize, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|    * Build the barcode reader widget | ||||
|    */ | ||||
|   Widget BarcodeReader(BuildContext context) { | ||||
|  | ||||
|     return ReaderWidget( | ||||
|       onScan: _onScanSuccess, | ||||
|       isMultiScan: false, | ||||
|       tryHarder: true, | ||||
|       tryInverted: true, | ||||
|       tryRotate: true, | ||||
|       showGallery: false, | ||||
|       scanDelay: Duration(milliseconds: scan_delay), | ||||
|       resolution: ResolutionPreset.high, | ||||
|       lensDirection: CameraLensDirection.back, | ||||
|       flashOnIcon: const Icon(Icons.flash_on), | ||||
|       flashOffIcon: const Icon(Icons.flash_off), | ||||
|       toggleCameraIcon: const Icon(TablerIcons.camera_rotate), | ||||
|       actionButtonsBackgroundBorderRadius: | ||||
|       BorderRadius.circular(40), | ||||
|       scannerOverlay: BarcodeOverlay(context), | ||||
|       actionButtonsBackgroundColor: Colors.black.withOpacity(0.7), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget topCenterOverlay() { | ||||
|     return SafeArea( | ||||
|       child: Align( | ||||
|         alignment: Alignment.topCenter, | ||||
|         child: Padding( | ||||
|           padding: EdgeInsets.all(10), | ||||
|           child: Text( | ||||
|             widget.handler.getOverlayText(context), | ||||
|             style: TextStyle( | ||||
|               color: Colors.white, | ||||
|               fontSize: 16, | ||||
|               fontWeight: FontWeight.bold | ||||
|             ) | ||||
|           ) | ||||
|         ) | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget bottomCenterOverlay() { | ||||
|  | ||||
|     String info_text = scanning_paused ? L10().barcodeScanPaused : L10().barcodeScanPause; | ||||
|  | ||||
|     return Scaffold( | ||||
|         appBar: AppBar( | ||||
|           backgroundColor: COLOR_APP_BAR, | ||||
|           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(); | ||||
|               }, | ||||
|     return SafeArea( | ||||
|       child: Align( | ||||
|         alignment: Alignment.bottomCenter, | ||||
|         child: Padding( | ||||
|           padding: EdgeInsets.all(10), | ||||
|           child: Text( | ||||
|               scanned_code.isNotEmpty ? scanned_code : info_text, | ||||
|               textAlign: TextAlign.center, | ||||
|               style: TextStyle( | ||||
|                 color: Colors.white, | ||||
|                 fontSize: 16, | ||||
|                 fontWeight: FontWeight.bold | ||||
|               ) | ||||
|           ), | ||||
|         ) | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|  | ||||
|   /* | ||||
|    *  Display an overlay at the bottom right of the screen | ||||
|    */ | ||||
|   Widget bottomRightOverlay() { | ||||
|     return SafeArea( | ||||
|         child: Align( | ||||
|             alignment: Alignment.bottomRight, | ||||
|             child: Padding( | ||||
|                 padding: EdgeInsets.all(10), | ||||
|                 child: ClipRRect( | ||||
|                     borderRadius: BorderRadius.circular(40), | ||||
|                     child: ColoredBox( | ||||
|                         color: Colors.black45, | ||||
|                         child: Row( | ||||
|                             mainAxisSize: MainAxisSize.min, | ||||
|                             children: scanning_paused ? [] : [ | ||||
|                               CircularProgressIndicator( | ||||
|                                   value: null | ||||
|                               ) | ||||
|                               // actionIcon, | ||||
|                             ] | ||||
|                         ) | ||||
|                     ) | ||||
|                 ) | ||||
|             ) | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|  | ||||
|     return Scaffold( | ||||
|       appBar: AppBar( | ||||
|         backgroundColor: COLOR_APP_BAR, | ||||
|         title: Text(L10().scanBarcode), | ||||
|       ), | ||||
|       body: GestureDetector( | ||||
|         onTap: () async { | ||||
|           setState(() { | ||||
|             scanning_paused = !scanning_paused; | ||||
|           }); | ||||
|         }, | ||||
|         child: Stack( | ||||
|           children: <Widget>[ | ||||
|             Column( | ||||
|               children: [ | ||||
|                 Expanded( | ||||
|                     child: BarcodeReader(context) | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|             topCenterOverlay(), | ||||
|             bottomCenterOverlay(), | ||||
|             bottomRightOverlay(), | ||||
|           ], | ||||
|         ), | ||||
|         body: GestureDetector( | ||||
|             onTapDown: (details) async { | ||||
|               setState(() { | ||||
|                 scanning_paused = !scanning_paused; | ||||
|               }); | ||||
|             }, | ||||
|             onLongPressEnd: (details) async { | ||||
|               if (mounted) { | ||||
|                 setState(() { | ||||
|                   scanning_paused = false; | ||||
|                 }); | ||||
|               } | ||||
|             }, | ||||
|             child: Stack( | ||||
|               children: <Widget>[ | ||||
|                 Column(children: [ | ||||
|                   Expanded( | ||||
|                       child: QRView( | ||||
|                     key: barcodeControllerKey, | ||||
|                     onQRViewCreated: (QRViewController controller) { | ||||
|                       _onViewCreated(context, controller); | ||||
|                     }, | ||||
|                     overlay: QrScannerOverlayShape( | ||||
|                       borderColor: | ||||
|                           scanning_paused ? COLOR_WARNING : COLOR_ACTION, | ||||
|                       borderRadius: 10, | ||||
|                       borderLength: 30, | ||||
|                       borderWidth: 10, | ||||
|                       cutOutSize: 300, | ||||
|                     ), | ||||
|                   )) | ||||
|                 ]), | ||||
|                 Center( | ||||
|                     child: Column(children: [ | ||||
|                   Padding( | ||||
|                       child: Text( | ||||
|                         widget.handler.getOverlayText(context), | ||||
|                         style: TextStyle( | ||||
|                           fontSize: 16, | ||||
|                             fontWeight: FontWeight.bold, color: Colors.white), | ||||
|                       ), | ||||
|                       padding: EdgeInsets.all(25)), | ||||
|                   Padding( | ||||
|                     child: CircularProgressIndicator( | ||||
|                         value: scanning_paused ? 0 : null), | ||||
|                     padding: EdgeInsets.all(40), | ||||
|                   ), | ||||
|                   Spacer(), | ||||
|                   SizedBox( | ||||
|                     child: Center( | ||||
|                       child: actionIcon, | ||||
|                     ), | ||||
|                     width: 100, | ||||
|                     height: 150, | ||||
|                   ), | ||||
|                   Padding( | ||||
|                     child: Text(info_text, | ||||
|                         textAlign: TextAlign.center, | ||||
|                         style: TextStyle( | ||||
|                           color: Colors.white, | ||||
|                         )), | ||||
|                     padding: EdgeInsets.all(25), | ||||
|                   ), | ||||
|                 ])) | ||||
|               ], | ||||
|             ))); | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -60,6 +60,7 @@ class BarcodeHandler { | ||||
|   Future<void> processBarcode(String barcode, | ||||
|       {String url = "barcode/", | ||||
|       Map<String, dynamic> extra_data = const {}}) async { | ||||
|  | ||||
|     debug("Scanned barcode data: '${barcode}'"); | ||||
|  | ||||
|     barcode = barcode.trim(); | ||||
|   | ||||
| @@ -274,6 +274,12 @@ | ||||
|   "damaged": "Damaged", | ||||
|   "@damaged": {}, | ||||
|  | ||||
|   "colorScheme": "Color Scheme", | ||||
|   "@colorScheme": {}, | ||||
|  | ||||
|   "colorSchemeDetail": "Select color scheme", | ||||
|   "@colorSchemeDetail": {}, | ||||
|  | ||||
|   "darkMode": "Dark Mode", | ||||
|   "@darkMode": {}, | ||||
|  | ||||
|   | ||||
| @@ -174,7 +174,7 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> { | ||||
|             ListTile( | ||||
|               title: Text(L10().darkMode), | ||||
|               subtitle: Text(L10().darkModeEnable), | ||||
|               leading: Icon(TablerIcons.moon), | ||||
|               leading: Icon(TablerIcons.sun_moon), | ||||
|               trailing: Switch( | ||||
|                 value: darkMode, | ||||
|                 onChanged: (bool value) { | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
|  | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:flutter_tabler_icons/flutter_tabler_icons.dart"; | ||||
| import "package:inventree/app_colors.dart"; | ||||
|  | ||||
| import "package:inventree/l10.dart"; | ||||
| import "package:inventree/preferences.dart"; | ||||
| @@ -39,7 +40,10 @@ class _InvenTreePurchaseOrderSettingsState extends State<InvenTreePurchaseOrderS | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|         appBar: AppBar(title: Text(L10().purchaseOrderSettings)), | ||||
|         appBar: AppBar( | ||||
|           title: Text(L10().purchaseOrderSettings), | ||||
|           backgroundColor: COLOR_APP_BAR, | ||||
|         ), | ||||
|         body: Container( | ||||
|             child: ListView( | ||||
|                 children: [ | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import "package:flutter/material.dart"; | ||||
| import "package:flutter_tabler_icons/flutter_tabler_icons.dart"; | ||||
|  | ||||
| import "package:inventree/l10.dart"; | ||||
| import "package:inventree/app_colors.dart"; | ||||
| import "package:inventree/preferences.dart"; | ||||
|  | ||||
|  | ||||
| @@ -39,7 +40,10 @@ class _InvenTreeSalesOrderSettingsState extends State<InvenTreeSalesOrderSetting | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|         appBar: AppBar(title: Text(L10().salesOrderSettings)), | ||||
|         appBar: AppBar( | ||||
|           title: Text(L10().salesOrderSettings), | ||||
|           backgroundColor: COLOR_APP_BAR, | ||||
|         ), | ||||
|         body: Container( | ||||
|             child: ListView( | ||||
|                 children: [ | ||||
|   | ||||
| @@ -266,6 +266,7 @@ class _InvenTreeSelectServerState extends State<InvenTreeSelectServerWidget> { | ||||
|       key: _loginKey, | ||||
|       appBar: AppBar( | ||||
|         title: Text(L10().profileSelect), | ||||
|         backgroundColor: COLOR_APP_BAR, | ||||
|         actions: [ | ||||
|           IconButton( | ||||
|             icon: Icon(TablerIcons.circle_plus), | ||||
|   | ||||
| @@ -208,11 +208,8 @@ class _AttachmentWidgetState extends RefreshableState<AttachmentWidget> { | ||||
|  | ||||
|     if (tiles.isEmpty) { | ||||
|       tiles.add(ListTile( | ||||
|         leading: Icon(TablerIcons.file_x, color: COLOR_WARNING), | ||||
|         title: Text(L10().attachmentNone), | ||||
|         subtitle: Text( | ||||
|           L10().attachmentNoneDetail, | ||||
|           style: TextStyle(fontStyle: FontStyle.italic), | ||||
|         ), | ||||
|       )); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import "package:adaptive_theme/adaptive_theme.dart"; | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:flutter_tabler_icons/flutter_tabler_icons.dart"; | ||||
|  | ||||
| @@ -185,6 +186,28 @@ class InvenTreeDrawer extends StatelessWidget { | ||||
|       ) | ||||
|     ); | ||||
|  | ||||
|     tiles.add(Divider()); | ||||
|  | ||||
|     bool darkMode = AdaptiveTheme.of(context).mode.isDark; | ||||
|  | ||||
|     tiles.add( | ||||
|       ListTile( | ||||
|         onTap: () { | ||||
|           AdaptiveTheme.of(context).toggleThemeMode(); | ||||
|           _closeDrawer(); | ||||
|         }, | ||||
|         title: Text(L10().colorScheme), | ||||
|         subtitle: Text(L10().colorSchemeDetail), | ||||
|         leading: Icon( | ||||
|           TablerIcons.sun_moon, | ||||
|           color: COLOR_ACTION | ||||
|         ), | ||||
|         trailing: Icon( | ||||
|           darkMode ? TablerIcons.moon : TablerIcons.sun, | ||||
|         ) | ||||
|       ) | ||||
|     ); | ||||
|  | ||||
|     tiles.add( | ||||
|       ListTile( | ||||
|         title: Text(L10().settings), | ||||
|   | ||||
| @@ -101,7 +101,7 @@ mixin BaseWidgetProperties { | ||||
|         }, | ||||
|       ), | ||||
|       IconButton( | ||||
|         icon: Icon(Icons.barcode_reader, color: COLOR_ACTION), | ||||
|         icon: Icon(TablerIcons.barcode, color: COLOR_ACTION), | ||||
|         iconSize: iconSize, | ||||
|         onPressed: () { | ||||
|           if (InvenTreeAPI().checkConnection()) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user