import "dart:math"; import "dart:typed_data"; import "package:camera/camera.dart"; import "package:flutter/material.dart"; import "package:flutter_speed_dial/flutter_speed_dial.dart"; import "package:flutter_tabler_icons/flutter_tabler_icons.dart"; import "package:inventree/app_colors.dart"; import "package:inventree/inventree/sentry.dart"; import "package:inventree/preferences.dart"; import "package:inventree/widget/snacks.dart"; import "package:mobile_scanner/mobile_scanner.dart"; import "package:one_context/one_context.dart"; import "package:wakelock_plus/wakelock_plus.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(); bool flash_status = false; int scan_delay = 500; bool single_scanning = false; bool scanning_paused = false; bool multiple_barcodes = false; String scanned_code = ""; final MobileScannerController controller = MobileScannerController( autoZoom: true ); @override void initState() { super.initState(); _loadSettings(); WakelockPlus.enable(); } @override void dispose() { super.dispose(); WakelockPlus.disable(); } /* * Load the barcode scanning settings */ Future _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; }); } } @override Future pauseScan() async { if (mounted) { setState(() { scanning_paused = true; }); } } @override Future resumeScan() async { controller.start(); if (mounted) { setState(() { scanning_paused = false; }); } } /* * Callback function when a barcode is scanned */ Future onScanSuccess(BarcodeCapture result) async { if (!mounted || scanning_paused) { return; } // TODO: Display outline of barcodes on the screen? if (result.barcodes.isEmpty) { setState(() { multiple_barcodes = false; }); } else if (result.barcodes.length > 1) { setState(() { multiple_barcodes = true; }); return; } else { setState(() { multiple_barcodes = false; }); } Uint8List rawData = result.barcodes.first.rawBytes ?? Uint8List(0); String barcode; if (rawData.isNotEmpty) { final buffer = StringBuffer(); for (int ii = 0; ii < rawData.length; ii++) { buffer.writeCharCode(rawData[ii]); } barcode = buffer.toString(); print(barcode); } else { // Fall back to text value barcode = result.barcodes.first.rawValue ?? ""; } if (barcode.isEmpty) { // TODO: Error message "empty barcode" return; } setState(() { scanned_code = barcode; }); pauseScan(); await handleBarcodeData(barcode).then((_) { if (!single_scanning && mounted) { resumeScan(); } }); resumeScan(); if (mounted) { setState(() { scanned_code = ""; multiple_barcodes = false; }); } } void onControllerCreated(CameraController? controller, Exception? error) { if (error != null) { sentryReportError( "CameraBarcodeController.onControllerCreated", error, null ); } if (controller == null) { showSnackIcon( L10().cameraCreationError, icon: TablerIcons.camera_x, success: false ); if (OneContext.hasContext) { Navigator.pop(OneContext().context!); } } } Widget BarcodeOverlay(BuildContext context) { final Size screenSize = MediaQuery.of(context).size; final double width = screenSize.width; final double height = screenSize.height; final double D = min(width, height) * 0.8; // Color for the barcode scan? Color overlayColor = COLOR_ACTION; if (multiple_barcodes) { overlayColor = COLOR_DANGER; } else if (scanned_code.isNotEmpty) { overlayColor = COLOR_SUCCESS; } else if (scanning_paused) { overlayColor = COLOR_WARNING; } return Stack( children: [ Center( child: Container( width: D, height: D, decoration: BoxDecoration( border: Border.all( color: overlayColor, width: 4, ), ), ) ) ] ); } /* * Build the barcode reader widget */ Widget BarcodeReader(BuildContext context) { final Size screenSize = MediaQuery.of(context).size; final double width = screenSize.width; final double height = screenSize.height; final double D = min(width, height) * 0.8; return MobileScanner( controller: controller, overlayBuilder: (context, constraints) { return BarcodeOverlay(context); }, scanWindow: Rect.fromCenter( center: Offset(width / 2, height / 2), width: D, height: D ), onDetect: (result) { onScanSuccess(result); }, ); } Widget topCenterOverlay() { return SafeArea( child: Align( alignment: Alignment.topCenter, child: Padding( padding: EdgeInsets.only( left: 10, right: 10, top: 75, bottom: 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; String text = scanned_code.isNotEmpty ? scanned_code : info_text; if (text.length > 50) { text = text.substring(0, 50) + "..."; } return SafeArea( child: Align( alignment: Alignment.bottomCenter, child: Padding( padding: EdgeInsets.only( left: 10, right: 10, top: 10, bottom: 75 ), child: Text( text, textAlign: TextAlign.center, style: TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold ) ), ) ) ); } Widget? buildActions(BuildContext context) { List actions = [ SpeedDialChild( child: Icon(flash_status ? TablerIcons.bulb_off : TablerIcons.bulb), label: L10().toggleTorch, onTap: () async { controller.toggleTorch(); if (mounted) { setState(() { flash_status = !flash_status; }); } } ), SpeedDialChild( child: Icon(TablerIcons.camera), label: L10().switchCamera, onTap: () async { controller.switchCamera(); } ) ]; return SpeedDial( icon: Icons.more_horiz, children: actions, ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: COLOR_APP_BAR, title: Text(L10().scanBarcode), ), floatingActionButton: buildActions(context), body: GestureDetector( onTap: () async { if (mounted) { setState(() { // Toggle the 'scan paused' state scanning_paused = !scanning_paused; }); } }, child: Stack( children: [ Column( children: [ Expanded( child: BarcodeReader(context) ), ], ), topCenterOverlay(), bottomCenterOverlay() ], ), ), ); } }