78 lines
2.3 KiB
JavaScript
78 lines
2.3 KiB
JavaScript
/**
|
|
* 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;
|
|
}
|