import { getLocalizedString } from "@olenbetong/appframe-core"; import type BarcodeReaderType from "dynamsoft-javascript-barcode"; // import BarcodeReader from "dynamsoft-javascript-barcode"; import { type RefObject, useEffect, useRef, useState } from "react"; declare global { interface Window { BarcodeReader: typeof BarcodeReaderType; } } // (window as any).BarcodeReader = BarcodeReader; const { BarcodeReader } = window; export function useScanner( rootRef: RefObject, canvasRef: RefObject, onRead: (barcode: string) => void, ) { const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState<{ message: string; icon: string } | null>(null); const [rects, setRects] = useState>([]); const [devices, setDevices] = useState({ all: [], current: { deviceId: "", label: "" }, width: 0, height: 0, }); const currentDevice = useRef(null); const onReadRef = useRef(onRead); const scanner = useRef(null); useEffect(() => { onReadRef.current = onRead; }, [onRead]); useEffect(() => { if (!rootRef.current) return; let cancel = false; const iPhone = /iPhone|iPad/.test(navigator.userAgent); scanner.current = new BarcodeReader.Scanner({ duplicateForgetTime: 1000, htmlElement: rootRef.current, videoSettings: { video: { width: { ideal: iPhone ? 1280 : 720 }, height: { ideal: iPhone ? 1280 : 720 }, facingMode: { ideal: "environment" }, }, }, runtimeSettings: { mBarcodeFormatIds: BarcodeReader.EnumBarcodeFormat.DATAMATRIX, mAntiDamageLevel: 3, mDeblurLevel: 0, }, onFrameRead: (results) => { const newRects: Array = []; for (let result of results) { newRects.push(result.LocalizationResult); } if (newRects.length > 0) { setRects((rects) => [...rects, ...newRects]); setTimeout(() => { if (!cancel) { setRects((rects) => rects.slice(newRects.length)); } }, 300); } }, onNewCodeRead: (txt) => { if (txt) { onReadRef.current(txt); } }, }); scanner.current .open() .then((videoDeviceInfo) => { if (cancel === false) { const { all, current } = videoDeviceInfo; currentDevice.current = 0; if (current?.deviceId) { for (let i = 0; i < all.length; i++) { if (all[i].deviceId === current.deviceId) { currentDevice.current = i; } } } setDevices(videoDeviceInfo); setIsLoading(false); } }) .catch((ex) => { if (cancel === false) { let error = ex.message || ex; setIsLoading(false); if (/Webassembly/.test(error)) { setError({ icon: "unsupported", message: getLocalizedString( "Your browser is not supported. A browser that supports WebAssembly is required.", ), }); } else if ( /Permission/i.test(error) || /video/i.test(error) || /device/i.test(error) || /Media/i.test(error) || /agent/.test(error) || /found/.test(error) ) setError({ icon: "camera", message: getLocalizedString("No camera available"), }); else { setError({ icon: "generic", message: error }); } } }); return () => { cancel = true; if (scanner.current && typeof scanner.current.close === "function") { scanner.current.close(); } }; }, [rootRef]); useEffect(() => { if (canvasRef.current) { const ctx = canvasRef.current.getContext("2d") as CanvasRenderingContext2D; ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height); ctx.fillStyle = "rgba(254,180,32,0.3)"; const { width, height } = canvasRef.current; const xRatio = width / devices.width; const yRatio = height / devices.height; for (let rect of rects) { const { X1, X2, X3, X4, Y1, Y2, Y3, Y4 } = rect; ctx.beginPath(); ctx.moveTo(X1 * xRatio, Y1 * yRatio); ctx.lineTo(X2 * xRatio, Y2 * yRatio); ctx.lineTo(X3 * xRatio, Y3 * yRatio); ctx.lineTo(X4 * xRatio, Y4 * yRatio); ctx.fill(); } } }, [devices, canvasRef, rects]); function setNextDevice() { currentDevice.current = ((currentDevice.current ?? 0) + 1) % devices.all.length; scanner.current?.play(devices.all[currentDevice.current].deviceId); } return { devices: devices.all, error, isLoading, setNextDevice }; }