From 68e9cd00c22307155c5bbf1369a6b0f96235a5fe Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Dec 2024 04:38:10 +0000 Subject: [PATCH 01/38] Implement new "general purpose" barcode scan dialog - Separated widgets for camera / keyboard / wedge scanner - UI / UX improvements --- .../barcodes/BarcodeCameraInput.tsx | 236 ++++++++++++++++++ .../src/components/barcodes/BarcodeInput.tsx | 128 ++++++++++ .../barcodes/BarcodeKeyboardInput.tsx | 47 ++++ .../components/barcodes/BarcodeScanDialog.tsx | 34 +++ .../barcodes/BarcodeScannerInput.tsx | 19 ++ .../src/components/buttons/ScanButton.tsx | 28 +-- .../src/components/items/ActionDropdown.tsx | 3 +- .../src/components/items/BarcodeInput.tsx | 60 ----- src/frontend/src/components/items/QRCode.tsx | 20 +- .../src/components/modals/QrCodeModal.tsx | 61 ----- src/frontend/src/contexts/ThemeContext.tsx | 2 - 11 files changed, 486 insertions(+), 152 deletions(-) create mode 100644 src/frontend/src/components/barcodes/BarcodeCameraInput.tsx create mode 100644 src/frontend/src/components/barcodes/BarcodeInput.tsx create mode 100644 src/frontend/src/components/barcodes/BarcodeKeyboardInput.tsx create mode 100644 src/frontend/src/components/barcodes/BarcodeScanDialog.tsx create mode 100644 src/frontend/src/components/barcodes/BarcodeScannerInput.tsx delete mode 100644 src/frontend/src/components/items/BarcodeInput.tsx delete mode 100644 src/frontend/src/components/modals/QrCodeModal.tsx diff --git a/src/frontend/src/components/barcodes/BarcodeCameraInput.tsx b/src/frontend/src/components/barcodes/BarcodeCameraInput.tsx new file mode 100644 index 000000000000..f74a6c2ece9c --- /dev/null +++ b/src/frontend/src/components/barcodes/BarcodeCameraInput.tsx @@ -0,0 +1,236 @@ +import { Trans, t } from '@lingui/macro'; +import { + ActionIcon, + Button, + Container, + Group, + Select, + Stack +} from '@mantine/core'; +import { useDocumentVisibility, useLocalStorage } from '@mantine/hooks'; +import { showNotification } from '@mantine/notifications'; +import { + IconCamera, + IconPlayerPlayFilled, + IconPlayerStopFilled, + IconX +} from '@tabler/icons-react'; +import { type CameraDevice, Html5Qrcode } from 'html5-qrcode'; +import { useEffect, useState } from 'react'; +import Expand from '../items/Expand'; +import type { BarcodeInputProps } from './BarcodeInput'; + +export default function BarcodeCameraInput({ + onScan, + placeholder = t`Start scanning by selecting a camera and pressing the play button.` +}: Readonly) { + const [qrCodeScanner, setQrCodeScanner] = useState(null); + const [camId, setCamId] = useLocalStorage({ + key: 'camId', + defaultValue: null + }); + const [cameras, setCameras] = useState([]); + const [cameraValue, setCameraValue] = useState(null); + const [scanningEnabled, setScanningEnabled] = useState(false); + const [wasAutoPaused, setWasAutoPaused] = useState(false); + const documentState = useDocumentVisibility(); + + let lastValue = ''; + + // Mount QR code once we are loaded + useEffect(() => { + setQrCodeScanner(new Html5Qrcode('reader')); + + // load cameras + Html5Qrcode.getCameras().then((devices) => { + if (devices?.length) { + setCameras(devices); + } + }); + }, []); + + // set camera value from id + useEffect(() => { + if (camId) { + setCameraValue(camId.id); + } + }, [camId]); + + // Stop/start when leaving or reentering page + useEffect(() => { + if (scanningEnabled && documentState === 'hidden') { + btnStopScanning(); + setWasAutoPaused(true); + } else if (wasAutoPaused && documentState === 'visible') { + btnStartScanning(); + setWasAutoPaused(false); + } + }, [documentState]); + + // Scanner functions + function onScanSuccess(decodedText: string) { + qrCodeScanner?.pause(); + + // dedouplication + if (decodedText === lastValue) { + qrCodeScanner?.resume(); + return; + } + lastValue = decodedText; + + // submit value upstream + onScan?.(decodedText); + + qrCodeScanner?.resume(); + } + + function onScanFailure(error: string) { + if ( + error != + 'QR code parse error, error = NotFoundException: No MultiFormat Readers were able to detect the code.' + ) { + console.warn(`Code scan error = ${error}`); + } + } + + // button handlers + function btnSelectCamera() { + Html5Qrcode.getCameras() + .then((devices) => { + if (devices?.length) { + setCamId(devices[0]); + } + }) + .catch((err) => { + showNotification({ + title: t`Error while getting camera`, + message: err, + color: 'red', + icon: + }); + }); + } + + function btnStartScanning() { + if (camId && qrCodeScanner && !scanningEnabled) { + qrCodeScanner + .start( + camId.id, + { fps: 10, qrbox: { width: 250, height: 250 } }, + (decodedText) => { + onScanSuccess(decodedText); + }, + (errorMessage) => { + onScanFailure(errorMessage); + } + ) + .catch((err: string) => { + showNotification({ + title: t`Error while scanning`, + message: err, + color: 'red', + icon: + }); + }); + setScanningEnabled(true); + } + } + + function btnStopScanning() { + if (qrCodeScanner && scanningEnabled) { + qrCodeScanner.stop().catch((err: string) => { + showNotification({ + title: t`Error while stopping`, + message: err, + color: 'red', + icon: + }); + }); + setScanningEnabled(false); + } + } + + // on value change + useEffect(() => { + if (cameraValue === null) return; + if (cameraValue === camId?.id) { + return; + } + + const cam = cameras.find((cam) => cam.id === cameraValue); + + // stop scanning if cam changed while scanning + if (qrCodeScanner && scanningEnabled) { + // stop scanning + qrCodeScanner.stop().then(() => { + // change ID + setCamId(cam); + // start scanning + qrCodeScanner.start( + cam.id, + { fps: 10, qrbox: { width: 250, height: 250 } }, + (decodedText) => { + onScanSuccess(decodedText); + }, + (errorMessage) => { + onScanFailure(errorMessage); + } + ); + }); + } else { + setCamId(cam); + } + }, [cameraValue]); + + return ( + + + + - - {inp} - + + ); } - -// region input stuff -enum InputMethod { - Manual = 'manually', - ImageBarcode = 'imageBarcode' -} - -export interface ScanInputInterface { - action: (items: ScanItem[]) => void; -} - -interface BarcodeInputProps { - action: (decodedText: string) => void; - notScanningPlaceholder?: string; -} - -function InputManual({ action }: Readonly) { - const [value, setValue] = useState(''); - - function btnAddItem() { - if (value === '') return; - - const new_item: ScanItem = { - id: randomId(), - ref: value, - data: { item: value }, - timestamp: new Date(), - source: InputMethod.Manual - }; - action([new_item]); - setValue(''); - } - - function btnAddDummyItem() { - const new_item: ScanItem = { - id: randomId(), - ref: 'Test item', - data: {}, - timestamp: new Date(), - source: InputMethod.Manual - }; - action([new_item]); - } - - return ( - <> - - setValue(event.currentTarget.value)} - onKeyDown={getHotkeyHandler([['Enter', btnAddItem]])} - /> - - - - - - {IS_DEV_OR_DEMO && ( - - )} - - ); -} - -/* Input that uses QR code detection from images */ -export function InputImageBarcode({ - action, - notScanningPlaceholder = t`Start scanning by selecting a camera and pressing the play button.` -}: Readonly) { - const [qrCodeScanner, setQrCodeScanner] = useState(null); - const [camId, setCamId] = useLocalStorage({ - key: 'camId', - defaultValue: null - }); - const [cameras, setCameras] = useState([]); - const [cameraValue, setCameraValue] = useState(null); - const [scanningEnabled, setScanningEnabled] = useState(false); - const [wasAutoPaused, setWasAutoPaused] = useState(false); - const documentState = useDocumentVisibility(); - - let lastValue = ''; - - // Mount QR code once we are loaded - useEffect(() => { - setQrCodeScanner(new Html5Qrcode('reader')); - - // load cameras - Html5Qrcode.getCameras().then((devices) => { - if (devices?.length) { - setCameras(devices); - } - }); - }, []); - - // set camera value from id - useEffect(() => { - if (camId) { - setCameraValue(camId.id); - } - }, [camId]); - - // Stop/start when leaving or reentering page - useEffect(() => { - if (scanningEnabled && documentState === 'hidden') { - btnStopScanning(); - setWasAutoPaused(true); - } else if (wasAutoPaused && documentState === 'visible') { - btnStartScanning(); - setWasAutoPaused(false); - } - }, [documentState]); - - // Scanner functions - function onScanSuccess(decodedText: string) { - qrCodeScanner?.pause(); - - // dedouplication - if (decodedText === lastValue) { - qrCodeScanner?.resume(); - return; - } - lastValue = decodedText; - - // submit value upstream - action(decodedText); - - qrCodeScanner?.resume(); - } - - function onScanFailure(error: string) { - if ( - error != - 'QR code parse error, error = NotFoundException: No MultiFormat Readers were able to detect the code.' - ) { - console.warn(`Code scan error = ${error}`); - } - } - - // button handlers - function btnSelectCamera() { - Html5Qrcode.getCameras() - .then((devices) => { - if (devices?.length) { - setCamId(devices[0]); - } - }) - .catch((err) => { - showNotification({ - title: t`Error while getting camera`, - message: err, - color: 'red', - icon: - }); - }); - } - - function btnStartScanning() { - if (camId && qrCodeScanner && !scanningEnabled) { - qrCodeScanner - .start( - camId.id, - { fps: 10, qrbox: { width: 250, height: 250 } }, - (decodedText) => { - onScanSuccess(decodedText); - }, - (errorMessage) => { - onScanFailure(errorMessage); - } - ) - .catch((err: string) => { - showNotification({ - title: t`Error while scanning`, - message: err, - color: 'red', - icon: - }); - }); - setScanningEnabled(true); - } - } - - function btnStopScanning() { - if (qrCodeScanner && scanningEnabled) { - qrCodeScanner.stop().catch((err: string) => { - showNotification({ - title: t`Error while stopping`, - message: err, - color: 'red', - icon: - }); - }); - setScanningEnabled(false); - } - } - - // on value change - useEffect(() => { - if (cameraValue === null) return; - if (cameraValue === camId?.id) { - return; - } - - const cam = cameras.find((cam) => cam.id === cameraValue); - - // stop scanning if cam changed while scanning - if (qrCodeScanner && scanningEnabled) { - // stop scanning - qrCodeScanner.stop().then(() => { - // change ID - setCamId(cam); - // start scanning - qrCodeScanner.start( - cam.id, - { fps: 10, qrbox: { width: 250, height: 250 } }, - (decodedText) => { - onScanSuccess(decodedText); - }, - (errorMessage) => { - onScanFailure(errorMessage); - } - ); - }); - } else { - setCamId(cam); - } - }, [cameraValue]); - - return ( - - -