pwa and mobile client

This commit is contained in:
azykov@mail.ru
2026-09-29 15:55:25 +03:00
parent 657009bd6c
commit 1935acf2ff
96 changed files with 8370 additions and 58 deletions
+86
View File
@@ -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);
}