Files

92 lines
3.2 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import { pushOverlayBack } from './platform.js';
import type { ScanResult } from './scanner.js';
import { Icon } from './ui.js';
type Formats = 'card' | 'qr';
/** Fullscreen camera view that resolves with the first detected code. */
export function ScannerView({ formats, hint, onResult, onClose }: { formats: Formats; hint: string; onResult: (result: ScanResult) => void; onClose: () => void }) {
const videoRef = useRef<HTMLVideoElement>(null);
const [error, setError] = useState<string>();
const onResultRef = useRef(onResult);
onResultRef.current = onResult;
// Android back button closes the scanner, not the screen underneath.
const closeRef = useRef(onClose);
closeRef.current = onClose;
useEffect(() => pushOverlayBack(closeRef), []);
useEffect(() => {
let stopped = false;
let stream: MediaStream | undefined;
let timer: ReturnType<typeof setTimeout> | undefined;
(async () => {
try {
const { createDetector, CARD_FORMATS, QR_FORMATS } = await import('./scanner.js');
const detect = createDetector(formats === 'qr' ? QR_FORMATS : CARD_FORMATS);
stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: { ideal: 'environment' }, width: { ideal: 1280 }, height: { ideal: 720 } },
audio: false,
});
if (stopped) return;
const video = videoRef.current!;
video.srcObject = stream;
await video.play();
const tick = async () => {
if (stopped) return;
try {
if (video.readyState >= 2) {
const found = await detect(video);
if (found && !stopped) {
stopped = true;
onResultRef.current(found);
return;
}
}
} catch {
// a frame that could not be decoded
}
timer = setTimeout(tick, 200);
};
void tick();
} catch (e) {
const name = (e as Error).name;
setError(
name === 'NotAllowedError'
? 'Camera access was denied. Allow it in the app or browser settings.'
: name === 'NotFoundError'
? 'No camera found.'
: `Cannot start the camera: ${(e as Error).message}`,
);
}
})();
return () => {
stopped = true;
if (timer) clearTimeout(timer);
stream?.getTracks().forEach((t) => t.stop());
};
}, [formats]);
return (
<div className="scanner">
<video ref={videoRef} playsInline muted />
<div className={`scanner-frame ${formats}`} />
<div className="scanner-top">
<button className="icon-button" onClick={onClose} aria-label="Close">
<Icon name="close" />
</button>
</div>
<div className="scanner-hint">{error ?? hint}</div>
</div>
);
}
/** Detects a code on a picked image file. */
export async function scanImageFile(file: Blob, formats: Formats): Promise<ScanResult | undefined> {
const { createDetector, CARD_FORMATS, QR_FORMATS } = await import('./scanner.js');
return createDetector(formats === 'qr' ? QR_FORMATS : CARD_FORMATS)(file);
}