132 lines
4.9 KiB
JavaScript
132 lines
4.9 KiB
JavaScript
// QR code scanner interop (html5-qrcode) for Blazor.
|
|
// Usage from .NET: qrScanner.start(elementId, dotNetRef) / qrScanner.stop()
|
|
window.qrScanner = (function () {
|
|
let scanner = null;
|
|
let lastText = null;
|
|
let lastTime = 0;
|
|
|
|
async function ensureLib() {
|
|
if (window.Html5Qrcode) return;
|
|
await new Promise((resolve, reject) => {
|
|
const s = document.createElement('script');
|
|
s.src = new URL('lib/html5-qrcode/html5-qrcode.min.js', document.baseURI).href;
|
|
s.onload = resolve;
|
|
s.onerror = () => reject(new Error('QR-Bibliothek konnte nicht geladen werden.'));
|
|
document.head.appendChild(s);
|
|
});
|
|
}
|
|
|
|
function createScanner(elementId) {
|
|
return new Html5Qrcode(elementId, {
|
|
formatsToSupport: [Html5QrcodeSupportedFormats.QR_CODE],
|
|
experimentalFeatures: { useBarCodeDetectorIfSupported: true },
|
|
verbose: false
|
|
});
|
|
}
|
|
|
|
function pickBackCamera(cams) {
|
|
const back = cams.find(c => /back|rear|environment|rück|hinten/i.test(c.label || ''));
|
|
return (back || cams[cams.length - 1]).id;
|
|
}
|
|
|
|
async function start(elementId, dotNetRef) {
|
|
await stop();
|
|
await ensureLib();
|
|
|
|
if (!window.isSecureContext || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
throw new Error('Live-Kamera nicht verfügbar (HTTPS erforderlich). Bitte „Foto aufnehmen“ verwenden.');
|
|
}
|
|
|
|
scanner = createScanner(elementId);
|
|
|
|
const config = {
|
|
fps: 10,
|
|
qrbox: (w, h) => {
|
|
const size = Math.floor(Math.max(150, Math.min(w, h) * 0.7));
|
|
return { width: Math.min(size, w), height: Math.min(size, h) };
|
|
},
|
|
disableFlip: false
|
|
};
|
|
|
|
const onSuccess = (text) => {
|
|
const now = Date.now();
|
|
if (text === lastText && now - lastTime < 2500) return;
|
|
lastText = text;
|
|
lastTime = now;
|
|
dotNetRef.invokeMethodAsync('OnQrScanned', text);
|
|
};
|
|
|
|
const attempts = [
|
|
() => scanner.start({ facingMode: { exact: 'environment' } }, config, onSuccess, () => { }),
|
|
() => scanner.start({ facingMode: 'environment' }, config, onSuccess, () => { }),
|
|
async () => {
|
|
const cams = await Html5Qrcode.getCameras();
|
|
if (!cams || cams.length === 0) throw new Error('Keine Kamera gefunden.');
|
|
await scanner.start(pickBackCamera(cams), config, onSuccess, () => { });
|
|
},
|
|
() => scanner.start({ facingMode: 'user' }, config, onSuccess, () => { })
|
|
];
|
|
|
|
let lastError = null;
|
|
for (const attempt of attempts) {
|
|
try {
|
|
await attempt();
|
|
return;
|
|
} catch (e) {
|
|
lastError = e;
|
|
if (e && (e.name === 'NotAllowedError' || /NotAllowed|Permission/i.test(String(e)))) break;
|
|
try { if (scanner.isScanning) await scanner.stop(); } catch { }
|
|
}
|
|
}
|
|
throw lastError instanceof Error ? lastError : new Error(String(lastError || 'Kamera konnte nicht gestartet werden.'));
|
|
}
|
|
|
|
// Decode a QR code from an image file (photo). Works on any device, also without HTTPS.
|
|
async function scanFile(elementId, inputId) {
|
|
await ensureLib();
|
|
const input = document.getElementById(inputId);
|
|
const file = input && input.files && input.files[0];
|
|
if (!file) return null;
|
|
await stop();
|
|
const fileScanner = createScanner(elementId);
|
|
try {
|
|
return await fileScanner.scanFile(file, false);
|
|
} catch (e) {
|
|
throw new Error('Kein QR-Code im Bild erkannt.');
|
|
} finally {
|
|
try { fileScanner.clear(); } catch { }
|
|
input.value = '';
|
|
}
|
|
}
|
|
|
|
async function stop() {
|
|
if (!scanner) return;
|
|
try {
|
|
if (scanner.isScanning) await scanner.stop();
|
|
scanner.clear();
|
|
} catch (e) { /* ignore */ }
|
|
scanner = null;
|
|
lastText = null;
|
|
}
|
|
|
|
// Short audio feedback ("success" | "error") via Web Audio, no asset required.
|
|
function playSound(type) {
|
|
try {
|
|
const Ctx = window.AudioContext || window.webkitAudioContext;
|
|
if (!Ctx) return;
|
|
const ctx = new Ctx();
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
osc.type = 'sine';
|
|
osc.frequency.value = type === 'error' ? 220 : 880;
|
|
gain.gain.value = 0.08;
|
|
osc.connect(gain).connect(ctx.destination);
|
|
osc.start();
|
|
osc.stop(ctx.currentTime + (type === 'error' ? 0.25 : 0.12));
|
|
osc.onended = () => ctx.close().catch(() => { });
|
|
} catch (e) { /* audio is best-effort */ }
|
|
}
|
|
|
|
return { start, stop, scanFile, playSound };
|
|
})();
|