// 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 }; })();