Add QR scanner (BarcodeDetector with manual-entry fallback)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user