From dafb2c6a099fce4b781792b39e9fe6956f7ed3fb Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Tue, 25 Jan 2022 10:53:36 +0200 Subject: [PATCH 1/6] QR Scanner first version --- packages/form-components/package.json | 3 +- .../component/QRCode/ReactQRScanner.story.tsx | 26 ++ .../src/component/QRCode/ReactQRScanner.tsx | 357 ++++++++++++++++++ yarn.lock | 5 + 4 files changed, 390 insertions(+), 1 deletion(-) create mode 100644 packages/form-components/src/component/QRCode/ReactQRScanner.story.tsx create mode 100644 packages/form-components/src/component/QRCode/ReactQRScanner.tsx diff --git a/packages/form-components/package.json b/packages/form-components/package.json index 8299fab2..5bb037f7 100644 --- a/packages/form-components/package.json +++ b/packages/form-components/package.json @@ -34,6 +34,7 @@ "primeflex": "2.0.0", "primeicons": "5.0.0", "primereact": "7.1.0", + "qr-scanner": "^1.3.0", "react": "17.0.2", "react-dom": "17.0.2", "react-img-mapper": "1.2.2", @@ -91,4 +92,4 @@ "/dist/" ] } -} +} \ No newline at end of file diff --git a/packages/form-components/src/component/QRCode/ReactQRScanner.story.tsx b/packages/form-components/src/component/QRCode/ReactQRScanner.story.tsx new file mode 100644 index 00000000..2ae3bea3 --- /dev/null +++ b/packages/form-components/src/component/QRCode/ReactQRScanner.story.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import { Meta, Story } from '@storybook/react'; +import ReactQRScanner, { ReactQRScannerProps } from './ReactQRScanner'; + +export default { + title: 'Components/QR Scanner', + component: ReactQRScanner, +} as Meta; + +// const Template: Story = (props) => ; +const Template: Story = (props) => ; + +export const OneFPS = Template.bind({}); +OneFPS.args = { fps: 1 }; + +export const FiveFPS = Template.bind({}); +FiveFPS.args = { fps: 5 }; + +export const TenFPS = Template.bind({}); +TenFPS.args = { fps: 10 }; + +export const NoAutostart = Template.bind({}); +NoAutostart.args = { autoStartScanning: false }; + +export const Default = Template.bind({}); +Default.args = {}; diff --git a/packages/form-components/src/component/QRCode/ReactQRScanner.tsx b/packages/form-components/src/component/QRCode/ReactQRScanner.tsx new file mode 100644 index 00000000..0059fa4f --- /dev/null +++ b/packages/form-components/src/component/QRCode/ReactQRScanner.tsx @@ -0,0 +1,357 @@ +import React, { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'; +import { useResizeDetector } from 'react-resize-detector'; +import QrScanner from 'qr-scanner'; +import QrScannerWorkerPath from '!!file-loader!../../../../../node_modules/qr-scanner/qr-scanner-worker.min.js'; +import { Button } from 'primereact/button'; +import { SplitButton } from 'primereact/splitbutton'; +import { ToggleButton } from 'primereact/togglebutton'; + +QrScanner.WORKER_PATH = QrScannerWorkerPath; + +// Used only when video isn't started and we can't get the real aspect ratio +const DEFAULT_ASPECT_RATIO = 1.333333333333333333333333333; +// const DEFAULT_ASPECT_RATIO = 1.77777; + +export interface ReactQRScannerProps { + autoStartScanning?: boolean; + fps?: number; + onQRCodeScanned?: (qrCode: string) => void; +} + +export const ReactQRScanner: FunctionComponent = ({ + fps = 30, + autoStartScanning = true, + onQRCodeScanned = (qrCode) => { + console.log(`Scanned QR Code: ${qrCode}`); + }, +}: ReactQRScannerProps) => { + const videoRef = useRef(null); + const canvasRef = useRef(null); + const qrScannerRef = useRef(null); + const [availableCameras, setAvailableCameras] = useState([]); + // TODO + const [selectedCamera, setSelectedCamera] = useState(null); + const [lastQRCode, setLastQRCode] = useState(null); + // const [scanRegion, setScanRegion] = useState(null); + const scanRegionRef = useRef(null); + const aspectRatioRef = useRef(DEFAULT_ASPECT_RATIO); + const scaleFactorRef = useRef(1); + + const [isStopped, setIsStopped] = useState(!autoStartScanning); + const isStoppedRef = useRef(!autoStartScanning); + + const isInitialised = useRef(false); + + const { width, height, ref: containerRef } = useResizeDetector(); + + async function initQrScanner(video: HTMLVideoElement) { + console.log('init', qrScannerRef.current); + qrScannerRef.current = new QrScanner(video, onScanned, onScanError, calculateVideoScanRegion); + qrScannerRef.current?.setInversionMode('both'); + + function availableWidth() { + const parentNode = videoRef.current?.parentNode?.parentNode as HTMLDivElement; + const availableWidth = parentNode === null ? 0 : parentNode.clientWidth; + return availableWidth; + } + + video.addEventListener('playing', () => { + aspectRatioRef.current = video.videoWidth / video.videoHeight; + isInitialised.current = true; + setCanvasSize(availableWidth()); + setUpDrawFrameToCanvas(); + }); + // The resize of the video can change the aspect ratio, so we have to recalculate it + video.addEventListener('resize', () => { + aspectRatioRef.current = video.videoWidth / video.videoHeight; + setCanvasSize(availableWidth()); + }); + } + + function cleanUpQrScanner() { + qrScannerRef.current?.stop(); + qrScannerRef.current?.destroy(); + qrScannerRef.current = null; + } + + async function startQrScanner() { + console.log('starting QrScanner'); + if (qrScannerRef.current === null) return; + try { + await qrScannerRef.current.start(); + console.log('QrScanner started'); + const cameras = await QrScanner.listCameras(true); + setAvailableCameras(cameras); + } catch (e) { + console.log(`ERROR while starting QR Scanner: ${e}`); + } + } + + function calculateScanRegion(width: number, height: number) { + const smallestDimension = Math.min(width, height); + // Original code: the scan region is two thirds of the smallest dimension of the video. + // const scanRegionSize = Math.round((2 / 3) * smallestDimension); + // We are going to go larger and use a scan region of 90% of the smallest dimension of the video. + const scanRegionSize = Math.round(smallestDimension * 0.9); + const legacyCanvasSize = 400; + return { + x: Math.round((width - scanRegionSize) / 2), + y: Math.round((height - scanRegionSize) / 2), + width: scanRegionSize, + height: scanRegionSize, + downScaledWidth: legacyCanvasSize, + downScaledHeight: legacyCanvasSize, + }; + } + + function calculateCanvasScanRegion(canvas: HTMLCanvasElement): QrScanner.ScanRegion { + return calculateScanRegion(canvas.width, canvas.height); + } + + function calculateVideoScanRegion(video: HTMLVideoElement): QrScanner.ScanRegion { + return calculateScanRegion(video.videoWidth, video.videoHeight); + } + + /* Init/Destroy to be called on mount/unmount */ + useEffect(() => { + console.log('Initialise ReactQRScanner'); + if (videoRef.current === null) return; + if (qrScannerRef.current !== null) return; + console.log('creating new QrScanner'); + + initQrScanner(videoRef.current); + if (autoStartScanning) { + // TODO started already? + // startQrScanner(); + } + return () => { + console.log('Cleaning up ReactQRScanner'); + cleanUpQrScanner(); + }; + }, []); + + /* Start/Stop called when isStopped state changes */ + useEffect(() => { + if (isStopped) { + qrScannerRef.current?.stop(); + } else { + startQrScanner(); + } + }, [isStopped]); + + function onScanned(qrCode: string) { + if (qrCode === '' || qrCode === lastQRCode) { + return; + } + setLastQRCode(qrCode); + } + + function onScanError(error: string) { + if (error === QrScanner.NO_QR_CODE_FOUND) return; + console.error('Scanning ERROR:', error); + } + + /* + Invoke the onQRCodeScanned action only when a new QR Code has been scanned. + The QrScanner component keeps re-calling the 'onScanned' callback above with the same QR code while the QR Code can be scanned. + We don't want to replicate this behaviour, so we maintain the lastQRCode that was scanned and emit an event only when a new QR Code was scanned. */ + useEffect(() => { + if (!lastQRCode) return; + onQRCodeScanned(lastQRCode); + }, [onQRCodeScanned, lastQRCode]); + + /* Resize Canvas and scanRegion when the parent DOM container node changes size */ + useEffect(() => { + if (!isInitialised.current) return; + const parentNode = videoRef.current?.parentNode?.parentNode as HTMLDivElement; + const availableWidth = parentNode === null ? 0 : parentNode.clientWidth; + + const video = videoRef.current; + if (video === null) return; + function aspectRatio() { + const video = videoRef.current; + + if (video === null || video.videoHeight === 0 || !isVideoReady()) { + return DEFAULT_ASPECT_RATIO; + } + return video.videoWidth / video.videoHeight; + } + aspectRatioRef.current = aspectRatio(); + + setCanvasSize(availableWidth); + }, [width, height, containerRef]); + + function setCanvasSize(availableWidth: number) { + // allow a safety margin otherwise resizing of the canvas resizes the parent container + // that triggers another resize effect and that gets us into a loop + const SAFETY_MARGIN = 10; + if (canvasRef.current === null) return; + if (videoRef.current === null) return; + const video = videoRef.current; + const canvas = canvasRef.current; + + aspectRatioRef.current = video.videoWidth / video.videoHeight; + + canvas.width = availableWidth - SAFETY_MARGIN; + canvas.height = Math.floor(canvas.width / aspectRatioRef.current); + + if (canvas !== null) { + scanRegionRef.current = calculateCanvasScanRegion(canvas); + } + } + + /* Set up the mechanism that shows the video frame on the canvas and draws the scan region area on top of it */ + function setUpDrawFrameToCanvas() { + let previousInvocationTs: number | null = null; + function drawFrameToCanvas() { + if (isStoppedRef.current) { + clearCanvas(); + return; + } + const canvas = canvasRef.current; + if (canvas === null) return; + const ctx = canvas.getContext('2d'); + if (ctx === null) return; + if (scanRegionRef.current === null) return; + + requestAnimationFrame((invocationTs) => { + if (previousInvocationTs !== null) { + const invocationDelta = invocationTs - previousInvocationTs; + if (invocationDelta < 1000 / fps) { + drawFrameToCanvas(); + return; + } + } + previousInvocationTs = invocationTs; + + if (videoRef.current === null) return; + const video = videoRef.current; + if (!isVideoReady()) { + drawFrameToCanvas(); + return; + } + + if (isVideoMirrored(video)) { + ctx.scale(-1, 1); + } + + ctx.drawImage(video, 0, 0, X(canvas.width), canvas.height); + if (scanRegionRef.current !== null) { + drawScanRegion(ctx, scanRegionRef.current, canvas.width); + } + + drawFrameToCanvas(); + }); + } + drawFrameToCanvas(); + + function X(x: number) { + return videoRef.current !== null && isVideoMirrored(videoRef.current) ? x * -1 : x; + } + + function drawScanRegion(ctx: CanvasRenderingContext2D, scanRegion: QrScanner.ScanRegion, canvasWidth: number) { + const scaleFactor = 1; + + const x = (scanRegion.x || 0) * scaleFactor; + const y = (scanRegion.y || 0) * scaleFactor; + const width = (scanRegion.width || 0) * scaleFactor; + const height = (scanRegion.height || 0) * scaleFactor; + + const side = width; + + ctx.lineWidth = canvasWidth > 500 ? 10 : 5; + ctx.strokeStyle = 'rgb(255, 165, 0, 0.7)'; + let cornerLineLength = width / 4; + + ctx.beginPath(); + ctx.moveTo(X(x - ctx.lineWidth / 2), y); + ctx.lineTo(X(x + cornerLineLength), y); + ctx.moveTo(X(x), y); + ctx.lineTo(X(x), y + cornerLineLength); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(X(x + side + ctx.lineWidth / 2), y); + ctx.lineTo(X(x + side - cornerLineLength), y); + ctx.moveTo(X(x + side), y); + ctx.lineTo(X(x + side), y + cornerLineLength); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(X(x + side + ctx.lineWidth / 2), y + height); + ctx.lineTo(X(x + side - cornerLineLength), y + height); + ctx.moveTo(X(x + side), y + height); + ctx.lineTo(X(x + side), y + height - cornerLineLength); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(X(x - ctx.lineWidth / 2), y + side); + ctx.lineTo(X(x + cornerLineLength), y + side); + ctx.moveTo(X(x), y + side); + ctx.lineTo(X(x), y + side - cornerLineLength); + ctx.stroke(); + } + } + + function clearCanvas() { + const canvas = canvasRef.current; + if (canvas === null) return; + const ctx = canvas.getContext('2d'); + if (ctx === null) return; + ctx.clearRect(0, 0, canvas.width, canvas.height); + // additional workaround needed to make sure the canvas is really cleared is to change the size of it + const w = canvas.width; + canvas.width = 0; + canvas.width = w; + } + + function startOrStop() { + const current = isStopped; + setIsStopped(!current); + isStoppedRef.current = !current; + } + + function isVideoReady() { + return videoRef.current !== null && videoRef.current?.readyState > 1; + } + + function isVideoMirrored(video: HTMLVideoElement) { + return video.style.transform === 'scaleX(-1)'; + } + + const cameras = availableCameras; + // TODO + const currentCamera = cameras.length > 0 ? cameras[0].label : null; + + return ( +
+
+ {currentCamera && } +
+
+
+ + +
+
+ +
+
+ ); +}; + +export default ReactQRScanner; diff --git a/yarn.lock b/yarn.lock index 878e1026..73f2f6bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16781,6 +16781,11 @@ q@^1.5.1: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= +qr-scanner@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/qr-scanner/-/qr-scanner-1.3.0.tgz#5a7cc7ae8edefc3ad0053a5473f591fb113f91ff" + integrity sha512-xNXlZaKOW0nihHaV7KPrMYJHNp1YX9z+NTqFrbNoibGIzQpPLeIocP9187lxihU/EbgplMm7sQ4hI9jG9+zYHg== + qs@6.7.0: version "6.7.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" From ba755b2b72447001f941bbe6a632228cefd8846b Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Tue, 8 Mar 2022 16:31:22 +0200 Subject: [PATCH 2/6] QRScanner Basic version of camera selection --- .../src/component/QRCode/ReactQRScanner.tsx | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/form-components/src/component/QRCode/ReactQRScanner.tsx b/packages/form-components/src/component/QRCode/ReactQRScanner.tsx index 0059fa4f..8585669f 100644 --- a/packages/form-components/src/component/QRCode/ReactQRScanner.tsx +++ b/packages/form-components/src/component/QRCode/ReactQRScanner.tsx @@ -2,9 +2,8 @@ import React, { FunctionComponent, useCallback, useEffect, useRef, useState } fr import { useResizeDetector } from 'react-resize-detector'; import QrScanner from 'qr-scanner'; import QrScannerWorkerPath from '!!file-loader!../../../../../node_modules/qr-scanner/qr-scanner-worker.min.js'; -import { Button } from 'primereact/button'; -import { SplitButton } from 'primereact/splitbutton'; import { ToggleButton } from 'primereact/togglebutton'; +import { Dropdown } from 'primereact/dropdown'; QrScanner.WORKER_PATH = QrScannerWorkerPath; @@ -29,7 +28,6 @@ export const ReactQRScanner: FunctionComponent = ({ const canvasRef = useRef(null); const qrScannerRef = useRef(null); const [availableCameras, setAvailableCameras] = useState([]); - // TODO const [selectedCamera, setSelectedCamera] = useState(null); const [lastQRCode, setLastQRCode] = useState(null); // const [scanRegion, setScanRegion] = useState(null); @@ -151,6 +149,19 @@ export const ReactQRScanner: FunctionComponent = ({ console.error('Scanning ERROR:', error); } + useEffect(() => { + /* TODO + - first try to setCamera, set in state only if no errors + - this method is async + - proper error handling + - what kind of errors would we get? Display some error if we can't set the camera? + - maybe display just 2 options? Front and Back (corresponding to user and environment in QrScanner terms) + */ + if (selectedCamera !== null) { + qrScannerRef.current?.setCamera(selectedCamera.id); + } + }, [selectedCamera]); + /* Invoke the onQRCodeScanned action only when a new QR Code has been scanned. The QrScanner component keeps re-calling the 'onScanned' callback above with the same QR code while the QR Code can be scanned. @@ -320,13 +331,21 @@ export const ReactQRScanner: FunctionComponent = ({ } const cameras = availableCameras; - // TODO - const currentCamera = cameras.length > 0 ? cameras[0].label : null; + const currentCamera = + selectedCamera === null ? (cameras.length > 0 ? cameras[0].label : null) : selectedCamera.label; return (
- {currentCamera && } + {currentCamera && ( + setSelectedCamera(e.value)} + /> + )}
From a72f9db8f8fe3eca3f630bda5be56ae741c675c5 Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Thu, 10 Mar 2022 14:35:20 +0200 Subject: [PATCH 3/6] Incorporate QR Scanner into Form Designer --- .../form-compiler/src/inputCompiler/index.ts | 4 ++ .../inputCompiler/qrScannerInputCompiler.ts | 24 ++++++++++ .../component/QRCode/ReactQRScanner.story.tsx | 3 ++ .../src/component/QRCode/ReactQRScanner.tsx | 5 ++- .../src/controls/QRCode/QRScannerControl.tsx | 25 +++++++++++ .../form-components/src/controls/index.ts | 3 ++ .../form-components/src/declarations.d.ts | 5 +++ packages/form-components/src/index.ts | 4 +- .../form-definition/src/interfaces/input.ts | 27 ++++++++---- packages/form-definition/src/schema/form.json | 44 +++++++++++++++++++ .../form-definition/src/schema/input.json | 44 +++++++++++++++++++ .../form-definition/src/schema/section.json | 44 +++++++++++++++++++ .../form-designer/src/component/Component.tsx | 9 ++++ .../src/component/FormPreview.story.tsx | 5 +++ .../component/biobank-definition.story.json | 5 +++ 15 files changed, 239 insertions(+), 12 deletions(-) create mode 100644 packages/form-compiler/src/inputCompiler/qrScannerInputCompiler.ts create mode 100644 packages/form-components/src/controls/QRCode/QRScannerControl.tsx diff --git a/packages/form-compiler/src/inputCompiler/index.ts b/packages/form-compiler/src/inputCompiler/index.ts index bc76824d..bfc1926d 100644 --- a/packages/form-compiler/src/inputCompiler/index.ts +++ b/packages/form-compiler/src/inputCompiler/index.ts @@ -17,6 +17,7 @@ import { MarkdownInputCompiler } from './markdownInputCompiler'; import { OptionsInputCompiler } from './optionsInputCompiler'; import { SignatureInputCompiler } from './signatureInputCompiler'; import { SampleContainerInputCompiler } from './sampleContainerInputCompiler'; +import { QRScannerInputCompiler } from './qrScannerInputCompiler'; export { AbstractInputCompiler, @@ -33,6 +34,8 @@ export { BooleanInputCompiler, DateTimeInputCompiler, SignatureInputCompiler, + QRScannerInputCompiler, + SampleContainerInputCompiler, }; export const inputCompilers: InputCompiler[] = [ @@ -49,6 +52,7 @@ export const inputCompilers: InputCompiler[] = [ new CountryInputCompiler(), new BooleanInputCompiler(), new DateTimeInputCompiler(), + new QRScannerInputCompiler(), new SampleContainerInputCompiler(), new SvgMapInputCompiler(), new SignatureInputCompiler(), diff --git a/packages/form-compiler/src/inputCompiler/qrScannerInputCompiler.ts b/packages/form-compiler/src/inputCompiler/qrScannerInputCompiler.ts new file mode 100644 index 00000000..d67bb00f --- /dev/null +++ b/packages/form-compiler/src/inputCompiler/qrScannerInputCompiler.ts @@ -0,0 +1,24 @@ +import { InputCompiler } from '../interfaces'; +import { JsonSchema, UISchemaElement } from '@jsonforms/core'; +import { Form, Input, InputType, Section } from '@eresearchqut/form-definition'; +import { AbstractInputCompiler } from './abstractInputCompiler'; + +export class QRScannerInputCompiler extends AbstractInputCompiler implements InputCompiler { + supports(form: Form, section: Section, input: Input): boolean { + return input.type === InputType.QR_SCANNER; + } + + schema(form: Form, section: Section, input: Input): JsonSchema { + return { + type: 'object', + properties: { + fps: { type: 'number' }, + autoStartScanning: { type: 'boolean' }, + }, + } as JsonSchema; + } + + ui(form: Form, section: Section, input: Input): UISchemaElement | undefined { + return this.uiControl(form, section, input); + } +} diff --git a/packages/form-components/src/component/QRCode/ReactQRScanner.story.tsx b/packages/form-components/src/component/QRCode/ReactQRScanner.story.tsx index 2ae3bea3..005441e9 100644 --- a/packages/form-components/src/component/QRCode/ReactQRScanner.story.tsx +++ b/packages/form-components/src/component/QRCode/ReactQRScanner.story.tsx @@ -22,5 +22,8 @@ TenFPS.args = { fps: 10 }; export const NoAutostart = Template.bind({}); NoAutostart.args = { autoStartScanning: false }; +export const SmallerVideo = Template.bind({}); +SmallerVideo.args = { videoMaxWidthPx: 320 }; + export const Default = Template.bind({}); Default.args = {}; diff --git a/packages/form-components/src/component/QRCode/ReactQRScanner.tsx b/packages/form-components/src/component/QRCode/ReactQRScanner.tsx index 8585669f..34098ff5 100644 --- a/packages/form-components/src/component/QRCode/ReactQRScanner.tsx +++ b/packages/form-components/src/component/QRCode/ReactQRScanner.tsx @@ -14,12 +14,14 @@ const DEFAULT_ASPECT_RATIO = 1.333333333333333333333333333; export interface ReactQRScannerProps { autoStartScanning?: boolean; fps?: number; + videoMaxWidthPx?: number; onQRCodeScanned?: (qrCode: string) => void; } export const ReactQRScanner: FunctionComponent = ({ fps = 30, autoStartScanning = true, + videoMaxWidthPx = Number.MIN_SAFE_INTEGER, onQRCodeScanned = (qrCode) => { console.log(`Scanned QR Code: ${qrCode}`); }, @@ -33,7 +35,6 @@ export const ReactQRScanner: FunctionComponent = ({ // const [scanRegion, setScanRegion] = useState(null); const scanRegionRef = useRef(null); const aspectRatioRef = useRef(DEFAULT_ASPECT_RATIO); - const scaleFactorRef = useRef(1); const [isStopped, setIsStopped] = useState(!autoStartScanning); const isStoppedRef = useRef(!autoStartScanning); @@ -203,7 +204,7 @@ export const ReactQRScanner: FunctionComponent = ({ aspectRatioRef.current = video.videoWidth / video.videoHeight; - canvas.width = availableWidth - SAFETY_MARGIN; + canvas.width = Math.min(availableWidth, videoMaxWidthPx) - SAFETY_MARGIN; canvas.height = Math.floor(canvas.width / aspectRatioRef.current); if (canvas !== null) { diff --git a/packages/form-components/src/controls/QRCode/QRScannerControl.tsx b/packages/form-components/src/controls/QRCode/QRScannerControl.tsx new file mode 100644 index 00000000..4e968e9d --- /dev/null +++ b/packages/form-components/src/controls/QRCode/QRScannerControl.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { and, ControlProps, ControlState, optionIs, RankedTester, rankWith, uiTypeIs } from '@jsonforms/core'; +import { Control, withJsonFormsControlProps } from '@jsonforms/react'; + +import merge from 'lodash/merge'; + +import { ReactQRScanner } from '../../component/QRCode/ReactQRScanner'; + +export class QRScannerControl extends Control { + render() { + const { data } = this.props; + const { fps, autoStartScanning } = data; + + return ( +
+ +
+ ); + } +} + +export const isQRScannerControl = and(uiTypeIs('Control'), optionIs('type', 'qr-scanner')); + +export const qrScannerControlTester: RankedTester = rankWith(2, isQRScannerControl); +export default withJsonFormsControlProps(QRScannerControl); diff --git a/packages/form-components/src/controls/index.ts b/packages/form-components/src/controls/index.ts index 789d71c2..42112e05 100644 --- a/packages/form-components/src/controls/index.ts +++ b/packages/form-components/src/controls/index.ts @@ -1,6 +1,7 @@ import InputControl, { inputControlTester } from './InputControl'; import InputBooleanControl, { inputBooleanControlTester } from './InputBooleanControl'; import SvgMapControl, { svgMapControlTester } from './SvgMapControl'; +import QRScannerControl, { qrScannerControlTester } from './QRCode/QRScannerControl'; import SampleContainerControl, { sampleContainerControlTester } from './Biobank/SampleContainerControl'; export { @@ -12,4 +13,6 @@ export { svgMapControlTester, SampleContainerControl, sampleContainerControlTester, + QRScannerControl, + qrScannerControlTester, }; diff --git a/packages/form-components/src/declarations.d.ts b/packages/form-components/src/declarations.d.ts index 3bb09f27..317ea957 100644 --- a/packages/form-components/src/declarations.d.ts +++ b/packages/form-components/src/declarations.d.ts @@ -1,2 +1,7 @@ declare module '*.scss'; declare module '*.svg'; + +// Allow importing JS modules +// this is to get around build errors after added QR Scanner +// TODO: check if there is a better solution than this workaround +declare module '*.js'; diff --git a/packages/form-components/src/index.ts b/packages/form-components/src/index.ts index b65b579d..b8d8eb6d 100644 --- a/packages/form-components/src/index.ts +++ b/packages/form-components/src/index.ts @@ -9,6 +9,8 @@ import { svgMapControlTester, SampleContainerControl, sampleContainerControlTester, + QRScannerControl, + qrScannerControlTester, } from './controls'; import { CategorizationLayout, @@ -54,7 +56,6 @@ import { InputTextCell, inputTextCellTester, } from './cells'; -import { sample } from 'lodash'; export * from './controls'; export * from './cells'; @@ -66,6 +67,7 @@ export const renderers: { tester: RankedTester; renderer: any }[] = [ { tester: inputBooleanControlTester, renderer: InputBooleanControl }, { tester: svgMapControlTester, renderer: SvgMapControl }, { tester: sampleContainerControlTester, renderer: SampleContainerControl }, + { tester: qrScannerControlTester, renderer: QRScannerControl }, { tester: verticalLayoutTester, renderer: VerticalLayout }, { tester: categorizationLayoutTester, renderer: CategorizationLayout }, { tester: categoryLayoutTester, renderer: CategoryLayout }, diff --git a/packages/form-definition/src/interfaces/input.ts b/packages/form-definition/src/interfaces/input.ts index 5f772cd0..ef7b2299 100644 --- a/packages/form-definition/src/interfaces/input.ts +++ b/packages/form-definition/src/interfaces/input.ts @@ -19,6 +19,7 @@ export enum InputType { MULTILINE_TEXT = 'multiline-text', NUMERIC = 'numeric', OPTIONS = 'options', + QR_SCANNER = 'qr-scanner', RANGE = 'range', SAMPLE_CONTAINER = 'biobank-sample-container', SIGNATURE = 'signature', @@ -103,15 +104,14 @@ export interface OptionsInput extends AbstractInput { /** * Option values and labels */ - options: - { - /** - * @format uuid - */ - id: string; - label: string; - value: number | string; - }[]; + options: { + /** + * @format uuid + */ + id: string; + label: string; + value: number | string; + }[]; } /** @@ -289,6 +289,14 @@ export interface CurrencyInput extends AbstractInput { maximum?: number; } +/** + * @title QR Scanner + * + */ +export interface QRScannerInput extends AbstractInput { + type: InputType.QR_SCANNER; +} + /** * @title Sample Container * @@ -331,6 +339,7 @@ export type Input = | MultilineTextInput | NumericInput | OptionsInput + | QRScannerInput | RangeInput | Signature | SampleContainerInput diff --git a/packages/form-definition/src/schema/form.json b/packages/form-definition/src/schema/form.json index 67bcf302..8c33cbc0 100644 --- a/packages/form-definition/src/schema/form.json +++ b/packages/form-definition/src/schema/form.json @@ -442,6 +442,9 @@ { "$ref": "#/definitions/CurrencyInput" }, + { + "$ref": "#/definitions/QRScannerInput" + }, { "$ref": "#/definitions/SampleContainerInput" }, @@ -757,6 +760,47 @@ "title": "Options", "type": "object" }, + "QRScannerInput": { + "properties": { + "countsToProgress": { + "description": "Does the input count to progress when completed", + "type": "boolean" + }, + "description": { + "description": "Description text can be used to provide more context that will help the user successfully complete the entry.", + "type": "string" + }, + "id": { + "format": "uuid", + "type": "string" + }, + "label": { + "description": "Label can be provided to override the name", + "type": "string" + }, + "name": { + "description": "The name of the element", + "type": "string" + }, + "required": { + "description": "Can the element be flagged as required", + "type": "boolean" + }, + "type": { + "enum": [ + "qr-scanner" + ], + "type": "string" + } + }, + "required": [ + "id", + "name", + "type" + ], + "title": "QR Scanner", + "type": "object" + }, "RangeInput": { "properties": { "countsToProgress": { diff --git a/packages/form-definition/src/schema/input.json b/packages/form-definition/src/schema/input.json index 889cce12..b13c56ac 100644 --- a/packages/form-definition/src/schema/input.json +++ b/packages/form-definition/src/schema/input.json @@ -46,6 +46,9 @@ { "$ref": "#/definitions/CurrencyInput" }, + { + "$ref": "#/definitions/QRScannerInput" + }, { "$ref": "#/definitions/SampleContainerInput" }, @@ -718,6 +721,47 @@ "title": "Options", "type": "object" }, + "QRScannerInput": { + "properties": { + "countsToProgress": { + "description": "Does the input count to progress when completed", + "type": "boolean" + }, + "description": { + "description": "Description text can be used to provide more context that will help the user successfully complete the entry.", + "type": "string" + }, + "id": { + "format": "uuid", + "type": "string" + }, + "label": { + "description": "Label can be provided to override the name", + "type": "string" + }, + "name": { + "description": "The name of the element", + "type": "string" + }, + "required": { + "description": "Can the element be flagged as required", + "type": "boolean" + }, + "type": { + "enum": [ + "qr-scanner" + ], + "type": "string" + } + }, + "required": [ + "id", + "name", + "type" + ], + "title": "QR Scanner", + "type": "object" + }, "RangeInput": { "properties": { "countsToProgress": { diff --git a/packages/form-definition/src/schema/section.json b/packages/form-definition/src/schema/section.json index 9cab36eb..9a926f67 100644 --- a/packages/form-definition/src/schema/section.json +++ b/packages/form-definition/src/schema/section.json @@ -662,6 +662,47 @@ "title": "Options", "type": "object" }, + "QRScannerInput": { + "properties": { + "countsToProgress": { + "description": "Does the input count to progress when completed", + "type": "boolean" + }, + "description": { + "description": "Description text can be used to provide more context that will help the user successfully complete the entry.", + "type": "string" + }, + "id": { + "format": "uuid", + "type": "string" + }, + "label": { + "description": "Label can be provided to override the name", + "type": "string" + }, + "name": { + "description": "The name of the element", + "type": "string" + }, + "required": { + "description": "Can the element be flagged as required", + "type": "boolean" + }, + "type": { + "enum": [ + "qr-scanner" + ], + "type": "string" + } + }, + "required": [ + "id", + "name", + "type" + ], + "title": "QR Scanner", + "type": "object" + }, "RangeInput": { "properties": { "countsToProgress": { @@ -1028,6 +1069,9 @@ { "$ref": "#/definitions/CurrencyInput" }, + { + "$ref": "#/definitions/QRScannerInput" + }, { "$ref": "#/definitions/SampleContainerInput" }, diff --git a/packages/form-designer/src/component/Component.tsx b/packages/form-designer/src/component/Component.tsx index 9f9732e9..a6108d96 100644 --- a/packages/form-designer/src/component/Component.tsx +++ b/packages/form-designer/src/component/Component.tsx @@ -22,6 +22,7 @@ import { faVial, faVectorSquare, IconDefinition, + faQrcode, } from '@fortawesome/free-solid-svg-icons'; import { faMarkdown } from '@fortawesome/free-brands-svg-icons'; import { InputType, SectionType } from '@eresearchqut/form-definition'; @@ -142,6 +143,14 @@ const componentDefaults: Map = new M description: 'Single or multiple choice from a list of options', }, ], + [ + InputType.QR_SCANNER, + { + icon: faQrcode, + label: 'QR Code Scanner', + description: 'Scans QR Codes using the camera', + }, + ], [ InputType.RANGE, { diff --git a/packages/form-designer/src/component/FormPreview.story.tsx b/packages/form-designer/src/component/FormPreview.story.tsx index d47ec10b..9997f824 100644 --- a/packages/form-designer/src/component/FormPreview.story.tsx +++ b/packages/form-designer/src/component/FormPreview.story.tsx @@ -46,6 +46,11 @@ export const BiobankExample = Template.bind({}); BiobankExample.args = { data: { biobank: { + qrScanner: { + fps: 10, + autoStartScanning: false, + videoMaxWidthPx: 480, + }, exampleTray: { width: 10, length: 10, diff --git a/packages/form-designer/src/component/biobank-definition.story.json b/packages/form-designer/src/component/biobank-definition.story.json index 51ad581c..55ed0ff0 100644 --- a/packages/form-designer/src/component/biobank-definition.story.json +++ b/packages/form-designer/src/component/biobank-definition.story.json @@ -8,6 +8,11 @@ "inputs": [ { "id": "6b0b5622-72cc-443c-b2d1-eead5272e451", + "type": "qr-scanner", + "name": "QR Scanner" + }, + { + "id": "6b0b5622-72cc-443c-b2d1-eead5272e452", "type": "biobank-sample-container", "name": "Example Tray" } From 975fb7057647ab8f7b435cd91ac2d8d50f30290d Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Wed, 16 Mar 2022 09:40:35 +0200 Subject: [PATCH 4/6] Simplifications after upgrade of qr-scanner lib. --- packages/form-components/package.json | 4 +- .../component/QRCode/ReactQRScanner.story.tsx | 10 - .../src/component/QRCode/ReactQRScanner.tsx | 287 +++--------------- .../form-components/src/declarations.d.ts | 5 - yarn.lock | 15 +- 5 files changed, 53 insertions(+), 268 deletions(-) diff --git a/packages/form-components/package.json b/packages/form-components/package.json index 5bb037f7..e0bb6919 100644 --- a/packages/form-components/package.json +++ b/packages/form-components/package.json @@ -34,7 +34,7 @@ "primeflex": "2.0.0", "primeicons": "5.0.0", "primereact": "7.1.0", - "qr-scanner": "^1.3.0", + "qr-scanner": "^1.4.1", "react": "17.0.2", "react-dom": "17.0.2", "react-img-mapper": "1.2.2", @@ -92,4 +92,4 @@ "/dist/" ] } -} \ No newline at end of file +} diff --git a/packages/form-components/src/component/QRCode/ReactQRScanner.story.tsx b/packages/form-components/src/component/QRCode/ReactQRScanner.story.tsx index 005441e9..6373f026 100644 --- a/packages/form-components/src/component/QRCode/ReactQRScanner.story.tsx +++ b/packages/form-components/src/component/QRCode/ReactQRScanner.story.tsx @@ -7,18 +7,8 @@ export default { component: ReactQRScanner, } as Meta; -// const Template: Story = (props) => ; const Template: Story = (props) => ; -export const OneFPS = Template.bind({}); -OneFPS.args = { fps: 1 }; - -export const FiveFPS = Template.bind({}); -FiveFPS.args = { fps: 5 }; - -export const TenFPS = Template.bind({}); -TenFPS.args = { fps: 10 }; - export const NoAutostart = Template.bind({}); NoAutostart.args = { autoStartScanning: false }; diff --git a/packages/form-components/src/component/QRCode/ReactQRScanner.tsx b/packages/form-components/src/component/QRCode/ReactQRScanner.tsx index 34098ff5..9cd07733 100644 --- a/packages/form-components/src/component/QRCode/ReactQRScanner.tsx +++ b/packages/form-components/src/component/QRCode/ReactQRScanner.tsx @@ -1,70 +1,43 @@ import React, { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'; import { useResizeDetector } from 'react-resize-detector'; import QrScanner from 'qr-scanner'; -import QrScannerWorkerPath from '!!file-loader!../../../../../node_modules/qr-scanner/qr-scanner-worker.min.js'; import { ToggleButton } from 'primereact/togglebutton'; import { Dropdown } from 'primereact/dropdown'; -QrScanner.WORKER_PATH = QrScannerWorkerPath; - -// Used only when video isn't started and we can't get the real aspect ratio -const DEFAULT_ASPECT_RATIO = 1.333333333333333333333333333; -// const DEFAULT_ASPECT_RATIO = 1.77777; +const GENERIC_CAMERAS: QrScanner.Camera[] = [ + { id: 'environment', label: 'Back Camera' }, + { id: 'user', label: 'Front Camera' }, +]; +const DEFAULT_CAMERA = GENERIC_CAMERAS[0]; export interface ReactQRScannerProps { autoStartScanning?: boolean; - fps?: number; - videoMaxWidthPx?: number; + videoMaxWidthPx?: number | null; onQRCodeScanned?: (qrCode: string) => void; } export const ReactQRScanner: FunctionComponent = ({ - fps = 30, autoStartScanning = true, - videoMaxWidthPx = Number.MIN_SAFE_INTEGER, + videoMaxWidthPx = null, onQRCodeScanned = (qrCode) => { console.log(`Scanned QR Code: ${qrCode}`); }, }: ReactQRScannerProps) => { const videoRef = useRef(null); - const canvasRef = useRef(null); const qrScannerRef = useRef(null); const [availableCameras, setAvailableCameras] = useState([]); - const [selectedCamera, setSelectedCamera] = useState(null); + const [selectedCamera, setSelectedCamera] = useState(DEFAULT_CAMERA); const [lastQRCode, setLastQRCode] = useState(null); - // const [scanRegion, setScanRegion] = useState(null); - const scanRegionRef = useRef(null); - const aspectRatioRef = useRef(DEFAULT_ASPECT_RATIO); - const [isStopped, setIsStopped] = useState(!autoStartScanning); - const isStoppedRef = useRef(!autoStartScanning); - - const isInitialised = useRef(false); - - const { width, height, ref: containerRef } = useResizeDetector(); async function initQrScanner(video: HTMLVideoElement) { - console.log('init', qrScannerRef.current); - qrScannerRef.current = new QrScanner(video, onScanned, onScanError, calculateVideoScanRegion); - qrScannerRef.current?.setInversionMode('both'); - - function availableWidth() { - const parentNode = videoRef.current?.parentNode?.parentNode as HTMLDivElement; - const availableWidth = parentNode === null ? 0 : parentNode.clientWidth; - return availableWidth; - } - - video.addEventListener('playing', () => { - aspectRatioRef.current = video.videoWidth / video.videoHeight; - isInitialised.current = true; - setCanvasSize(availableWidth()); - setUpDrawFrameToCanvas(); - }); - // The resize of the video can change the aspect ratio, so we have to recalculate it - video.addEventListener('resize', () => { - aspectRatioRef.current = video.videoWidth / video.videoHeight; - setCanvasSize(availableWidth()); + qrScannerRef.current = new QrScanner(video, onScanned, { + onDecodeError: onScanError, + calculateScanRegion: calculateScanRegion, + highlightScanRegion: true, + highlightCodeOutline: true, }); + qrScannerRef.current?.setInversionMode('both'); } function cleanUpQrScanner() { @@ -74,7 +47,6 @@ export const ReactQRScanner: FunctionComponent = ({ } async function startQrScanner() { - console.log('starting QrScanner'); if (qrScannerRef.current === null) return; try { await qrScannerRef.current.start(); @@ -86,16 +58,16 @@ export const ReactQRScanner: FunctionComponent = ({ } } - function calculateScanRegion(width: number, height: number) { - const smallestDimension = Math.min(width, height); + function calculateScanRegion(video: HTMLVideoElement) { + const smallestDimension = Math.min(video.videoWidth, video.videoHeight); // Original code: the scan region is two thirds of the smallest dimension of the video. // const scanRegionSize = Math.round((2 / 3) * smallestDimension); // We are going to go larger and use a scan region of 90% of the smallest dimension of the video. const scanRegionSize = Math.round(smallestDimension * 0.9); const legacyCanvasSize = 400; return { - x: Math.round((width - scanRegionSize) / 2), - y: Math.round((height - scanRegionSize) / 2), + x: Math.round((video.videoWidth - scanRegionSize) / 2), + y: Math.round((video.videoHeight - scanRegionSize) / 2), width: scanRegionSize, height: scanRegionSize, downScaledWidth: legacyCanvasSize, @@ -103,28 +75,12 @@ export const ReactQRScanner: FunctionComponent = ({ }; } - function calculateCanvasScanRegion(canvas: HTMLCanvasElement): QrScanner.ScanRegion { - return calculateScanRegion(canvas.width, canvas.height); - } - - function calculateVideoScanRegion(video: HTMLVideoElement): QrScanner.ScanRegion { - return calculateScanRegion(video.videoWidth, video.videoHeight); - } - /* Init/Destroy to be called on mount/unmount */ useEffect(() => { - console.log('Initialise ReactQRScanner'); if (videoRef.current === null) return; if (qrScannerRef.current !== null) return; - console.log('creating new QrScanner'); - initQrScanner(videoRef.current); - if (autoStartScanning) { - // TODO started already? - // startQrScanner(); - } return () => { - console.log('Cleaning up ReactQRScanner'); cleanUpQrScanner(); }; }, []); @@ -138,7 +94,14 @@ export const ReactQRScanner: FunctionComponent = ({ } }, [isStopped]); - function onScanned(qrCode: string) { + useEffect(() => { + if (selectedCamera !== null) { + qrScannerRef.current?.setCamera(selectedCamera.id); + } + }, [selectedCamera]); + + function onScanned(qrCodeData: QrScanner.ScanResult) { + const qrCode = qrCodeData.data; if (qrCode === '' || qrCode === lastQRCode) { return; } @@ -150,19 +113,6 @@ export const ReactQRScanner: FunctionComponent = ({ console.error('Scanning ERROR:', error); } - useEffect(() => { - /* TODO - - first try to setCamera, set in state only if no errors - - this method is async - - proper error handling - - what kind of errors would we get? Display some error if we can't set the camera? - - maybe display just 2 options? Front and Back (corresponding to user and environment in QrScanner terms) - */ - if (selectedCamera !== null) { - qrScannerRef.current?.setCamera(selectedCamera.id); - } - }, [selectedCamera]); - /* Invoke the onQRCodeScanned action only when a new QR Code has been scanned. The QrScanner component keeps re-calling the 'onScanned' callback above with the same QR code while the QR Code can be scanned. @@ -172,193 +122,36 @@ export const ReactQRScanner: FunctionComponent = ({ onQRCodeScanned(lastQRCode); }, [onQRCodeScanned, lastQRCode]); - /* Resize Canvas and scanRegion when the parent DOM container node changes size */ - useEffect(() => { - if (!isInitialised.current) return; - const parentNode = videoRef.current?.parentNode?.parentNode as HTMLDivElement; - const availableWidth = parentNode === null ? 0 : parentNode.clientWidth; - - const video = videoRef.current; - if (video === null) return; - function aspectRatio() { - const video = videoRef.current; - - if (video === null || video.videoHeight === 0 || !isVideoReady()) { - return DEFAULT_ASPECT_RATIO; - } - return video.videoWidth / video.videoHeight; - } - aspectRatioRef.current = aspectRatio(); - - setCanvasSize(availableWidth); - }, [width, height, containerRef]); - - function setCanvasSize(availableWidth: number) { - // allow a safety margin otherwise resizing of the canvas resizes the parent container - // that triggers another resize effect and that gets us into a loop - const SAFETY_MARGIN = 10; - if (canvasRef.current === null) return; - if (videoRef.current === null) return; - const video = videoRef.current; - const canvas = canvasRef.current; - - aspectRatioRef.current = video.videoWidth / video.videoHeight; - - canvas.width = Math.min(availableWidth, videoMaxWidthPx) - SAFETY_MARGIN; - canvas.height = Math.floor(canvas.width / aspectRatioRef.current); - - if (canvas !== null) { - scanRegionRef.current = calculateCanvasScanRegion(canvas); - } - } - - /* Set up the mechanism that shows the video frame on the canvas and draws the scan region area on top of it */ - function setUpDrawFrameToCanvas() { - let previousInvocationTs: number | null = null; - function drawFrameToCanvas() { - if (isStoppedRef.current) { - clearCanvas(); - return; - } - const canvas = canvasRef.current; - if (canvas === null) return; - const ctx = canvas.getContext('2d'); - if (ctx === null) return; - if (scanRegionRef.current === null) return; - - requestAnimationFrame((invocationTs) => { - if (previousInvocationTs !== null) { - const invocationDelta = invocationTs - previousInvocationTs; - if (invocationDelta < 1000 / fps) { - drawFrameToCanvas(); - return; - } - } - previousInvocationTs = invocationTs; - - if (videoRef.current === null) return; - const video = videoRef.current; - if (!isVideoReady()) { - drawFrameToCanvas(); - return; - } - - if (isVideoMirrored(video)) { - ctx.scale(-1, 1); - } - - ctx.drawImage(video, 0, 0, X(canvas.width), canvas.height); - if (scanRegionRef.current !== null) { - drawScanRegion(ctx, scanRegionRef.current, canvas.width); - } - - drawFrameToCanvas(); - }); - } - drawFrameToCanvas(); - - function X(x: number) { - return videoRef.current !== null && isVideoMirrored(videoRef.current) ? x * -1 : x; - } - - function drawScanRegion(ctx: CanvasRenderingContext2D, scanRegion: QrScanner.ScanRegion, canvasWidth: number) { - const scaleFactor = 1; - - const x = (scanRegion.x || 0) * scaleFactor; - const y = (scanRegion.y || 0) * scaleFactor; - const width = (scanRegion.width || 0) * scaleFactor; - const height = (scanRegion.height || 0) * scaleFactor; - - const side = width; - - ctx.lineWidth = canvasWidth > 500 ? 10 : 5; - ctx.strokeStyle = 'rgb(255, 165, 0, 0.7)'; - let cornerLineLength = width / 4; - - ctx.beginPath(); - ctx.moveTo(X(x - ctx.lineWidth / 2), y); - ctx.lineTo(X(x + cornerLineLength), y); - ctx.moveTo(X(x), y); - ctx.lineTo(X(x), y + cornerLineLength); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(X(x + side + ctx.lineWidth / 2), y); - ctx.lineTo(X(x + side - cornerLineLength), y); - ctx.moveTo(X(x + side), y); - ctx.lineTo(X(x + side), y + cornerLineLength); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(X(x + side + ctx.lineWidth / 2), y + height); - ctx.lineTo(X(x + side - cornerLineLength), y + height); - ctx.moveTo(X(x + side), y + height); - ctx.lineTo(X(x + side), y + height - cornerLineLength); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(X(x - ctx.lineWidth / 2), y + side); - ctx.lineTo(X(x + cornerLineLength), y + side); - ctx.moveTo(X(x), y + side); - ctx.lineTo(X(x), y + side - cornerLineLength); - ctx.stroke(); - } - } - - function clearCanvas() { - const canvas = canvasRef.current; - if (canvas === null) return; - const ctx = canvas.getContext('2d'); - if (ctx === null) return; - ctx.clearRect(0, 0, canvas.width, canvas.height); - // additional workaround needed to make sure the canvas is really cleared is to change the size of it - const w = canvas.width; - canvas.width = 0; - canvas.width = w; - } - function startOrStop() { - const current = isStopped; - setIsStopped(!current); - isStoppedRef.current = !current; + setIsStopped(!isStopped); } - function isVideoReady() { - return videoRef.current !== null && videoRef.current?.readyState > 1; - } + const possibleCameras = [ + { label: 'Generic', items: GENERIC_CAMERAS }, + { label: 'Specific', items: availableCameras }, + ]; - function isVideoMirrored(video: HTMLVideoElement) { - return video.style.transform === 'scaleX(-1)'; + const videoStyle: React.CSSProperties = {}; + if (videoMaxWidthPx) { + videoStyle['maxWidth'] = videoMaxWidthPx; } - const cameras = availableCameras; - const currentCamera = - selectedCamera === null ? (cameras.length > 0 ? cameras[0].label : null) : selectedCamera.label; - return (
- {currentCamera && ( + {availableCameras && ( setSelectedCamera(e.value)} /> )}
-
-
- - -
+
Date: Wed, 16 Mar 2022 10:26:33 +0200 Subject: [PATCH 5/6] Fix onDecodeError signature problem --- .../form-components/src/component/QRCode/ReactQRScanner.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/form-components/src/component/QRCode/ReactQRScanner.tsx b/packages/form-components/src/component/QRCode/ReactQRScanner.tsx index 9cd07733..41798513 100644 --- a/packages/form-components/src/component/QRCode/ReactQRScanner.tsx +++ b/packages/form-components/src/component/QRCode/ReactQRScanner.tsx @@ -1,5 +1,4 @@ import React, { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'; -import { useResizeDetector } from 'react-resize-detector'; import QrScanner from 'qr-scanner'; import { ToggleButton } from 'primereact/togglebutton'; import { Dropdown } from 'primereact/dropdown'; @@ -108,7 +107,7 @@ export const ReactQRScanner: FunctionComponent = ({ setLastQRCode(qrCode); } - function onScanError(error: string) { + function onScanError(error: Error | string) { if (error === QrScanner.NO_QR_CODE_FOUND) return; console.error('Scanning ERROR:', error); } From 735470abfc33cfc4d424256290b5ccf296597a6b Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Wed, 16 Mar 2022 10:32:12 +0200 Subject: [PATCH 6/6] Upgrade qr-scanner lib - form designer adaptations --- .../form-compiler/src/inputCompiler/qrScannerInputCompiler.ts | 2 +- .../form-components/src/controls/QRCode/QRScannerControl.tsx | 4 +--- packages/form-designer/src/component/FormPreview.story.tsx | 1 - 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/form-compiler/src/inputCompiler/qrScannerInputCompiler.ts b/packages/form-compiler/src/inputCompiler/qrScannerInputCompiler.ts index d67bb00f..44dd4178 100644 --- a/packages/form-compiler/src/inputCompiler/qrScannerInputCompiler.ts +++ b/packages/form-compiler/src/inputCompiler/qrScannerInputCompiler.ts @@ -12,8 +12,8 @@ export class QRScannerInputCompiler extends AbstractInputCompiler implements Inp return { type: 'object', properties: { - fps: { type: 'number' }, autoStartScanning: { type: 'boolean' }, + videoMaxWidthPx: { type: 'number' }, }, } as JsonSchema; } diff --git a/packages/form-components/src/controls/QRCode/QRScannerControl.tsx b/packages/form-components/src/controls/QRCode/QRScannerControl.tsx index 4e968e9d..38b3bf36 100644 --- a/packages/form-components/src/controls/QRCode/QRScannerControl.tsx +++ b/packages/form-components/src/controls/QRCode/QRScannerControl.tsx @@ -2,8 +2,6 @@ import React from 'react'; import { and, ControlProps, ControlState, optionIs, RankedTester, rankWith, uiTypeIs } from '@jsonforms/core'; import { Control, withJsonFormsControlProps } from '@jsonforms/react'; -import merge from 'lodash/merge'; - import { ReactQRScanner } from '../../component/QRCode/ReactQRScanner'; export class QRScannerControl extends Control { @@ -13,7 +11,7 @@ export class QRScannerControl extends Control { return (
- +
); } diff --git a/packages/form-designer/src/component/FormPreview.story.tsx b/packages/form-designer/src/component/FormPreview.story.tsx index 9997f824..6240a710 100644 --- a/packages/form-designer/src/component/FormPreview.story.tsx +++ b/packages/form-designer/src/component/FormPreview.story.tsx @@ -47,7 +47,6 @@ BiobankExample.args = { data: { biobank: { qrScanner: { - fps: 10, autoStartScanning: false, videoMaxWidthPx: 480, },