QR-Scanner: Kamera-Fallbacks und Foto-Upload; weitere Aenderungen
This commit is contained in:
1 parent
7a0406eaa2
commit
e2aef67099
13 files changed
+965
-58
No files matched your search
@@ -49,6 +49,31 @@
|
||||
} catch (e) { /* ignore */ }
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// File download from base64 (used for PDF exports)
|
||||
// ---------------------------------------------------------
|
||||
window.appInterop.downloadFile = function (fileName, base64, mimeType) {
|
||||
try {
|
||||
const bin = atob(base64);
|
||||
const bytes = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
||||
const blob = new Blob([bytes], { type: mimeType || 'application/octet-stream' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fileName || 'download';
|
||||
a.style.display = 'none';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
setTimeout(function () { URL.revokeObjectURL(url); }, 1000);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.warn('[appInterop] downloadFile failed', e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// beforeunload: warn user when draft flag is set
|
||||
// ---------------------------------------------------------
|
||||
|
||||
@@ -9,33 +9,43 @@ window.qrScanner = (function () {
|
||||
if (window.Html5Qrcode) return;
|
||||
await new Promise((resolve, reject) => {
|
||||
const s = document.createElement('script');
|
||||
s.src = 'lib/html5-qrcode/html5-qrcode.min.js';
|
||||
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 (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
throw new Error('Kamera wird von diesem Browser nicht unterstützt (HTTPS erforderlich).');
|
||||
if (!window.isSecureContext || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
throw new Error('Live-Kamera nicht verfügbar (HTTPS erforderlich). Bitte „Foto aufnehmen“ verwenden.');
|
||||
}
|
||||
|
||||
scanner = new Html5Qrcode(elementId, {
|
||||
formatsToSupport: [Html5QrcodeSupportedFormats.QR_CODE],
|
||||
verbose: false
|
||||
});
|
||||
scanner = createScanner(elementId);
|
||||
|
||||
const config = {
|
||||
fps: 10,
|
||||
qrbox: (w, h) => {
|
||||
const size = Math.max(180, Math.min(w, h) * 0.7);
|
||||
return { width: size, height: size };
|
||||
const size = Math.floor(Math.max(150, Math.min(w, h) * 0.7));
|
||||
return { width: Math.min(size, w), height: Math.min(size, h) };
|
||||
},
|
||||
aspectRatio: 1.0
|
||||
disableFlip: false
|
||||
};
|
||||
|
||||
const onSuccess = (text) => {
|
||||
@@ -46,13 +56,46 @@ window.qrScanner = (function () {
|
||||
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 {
|
||||
await scanner.start({ facingMode: 'environment' }, config, onSuccess, () => { });
|
||||
return await fileScanner.scanFile(file, false);
|
||||
} catch (e) {
|
||||
// Fallback: any camera
|
||||
const cams = await Html5Qrcode.getCameras();
|
||||
if (!cams || cams.length === 0) throw new Error('Keine Kamera gefunden.');
|
||||
await scanner.start(cams[0].id, config, onSuccess, () => { });
|
||||
throw new Error('Kein QR-Code im Bild erkannt.');
|
||||
} finally {
|
||||
try { fileScanner.clear(); } catch { }
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,5 +127,5 @@ window.qrScanner = (function () {
|
||||
} catch (e) { /* audio is best-effort */ }
|
||||
}
|
||||
|
||||
return { start, stop, playSound };
|
||||
return { start, stop, scanFile, playSound };
|
||||
})();
|
||||
Reference in new issue
Block a user