Add QR scanner (BarcodeDetector with manual-entry fallback)

This commit is contained in:
2026-06-03 09:59:21 +10:00
parent 09f157e2a4
commit bc8acba4e4
+77
View File
@@ -0,0 +1,77 @@
/**
* Lightweight QR scanning. Uses the native BarcodeDetector API when present
* (Android Chrome). Falls back to manual code entry where it isn't (iOS Safari
* has no BarcodeDetector). Returns the decoded payload string, or null if the
* user cancels.
*/
export async function scanQR() {
if ("BarcodeDetector" in window) {
try {
const formats = await window.BarcodeDetector.getSupportedFormats();
if (formats.includes("qr_code")) return await scanWithCamera();
} catch (_) { /* fall through to manual */ }
}
return manualEntry();
}
async function scanWithCamera() {
const detector = new window.BarcodeDetector({ formats: ["qr_code"] });
const overlay = buildOverlay();
const video = overlay.querySelector("video");
document.body.appendChild(overlay);
let stream;
try {
stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: { ideal: "environment" } }, audio: false,
});
} catch {
overlay.remove();
return manualEntry();
}
video.srcObject = stream;
await video.play().catch(() => {});
return new Promise((resolve) => {
let done = false;
const stop = (val) => {
if (done) return; done = true;
stream.getTracks().forEach((t) => t.stop());
overlay.remove();
resolve(val);
};
overlay.querySelector(".qr-cancel").onclick = () => stop(null);
overlay.querySelector(".qr-manual").onclick = async () => { stop(null); resolve(await manualEntry()); };
const tick = async () => {
if (done) return;
try {
const codes = await detector.detect(video);
if (codes.length) return stop(codes[0].rawValue);
} catch (_) { /* keep trying */ }
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
});
}
function manualEntry() {
const code = window.prompt(
"Enter the set code (printed beneath its QR), e.g. SET-GRAVEYARD:"
);
return code ? code.trim() : null;
}
function buildOverlay() {
const el = document.createElement("div");
el.className = "qr-overlay";
el.innerHTML = `
<video playsinline muted></video>
<div class="qr-frame"></div>
<p class="qr-hint">Point at a set's QR code</p>
<div class="qr-actions">
<button class="qr-manual">Enter code manually</button>
<button class="qr-cancel">Cancel</button>
</div>`;
return el;
}