pwa and mobile client
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user