Erste Produktivversion
This commit is contained in:
171
web/html/js/views/scanner.js
Normal file
171
web/html/js/views/scanner.js
Normal file
@@ -0,0 +1,171 @@
|
||||
// Kamerasucher für Strichcodes.
|
||||
//
|
||||
// Zwei Wege, in dieser Reihenfolge:
|
||||
// 1. BarcodeDetector - nativ in Chrome und auf Android, erkennt auch
|
||||
// QR- und Datamatrix-Codes, läuft außerhalb des Hauptthreads.
|
||||
// 2. Eigener Decoder aus barcode.js - überall sonst, insbesondere
|
||||
// Safari auf iPhone und iPad.
|
||||
//
|
||||
// Wenn beides ausfällt (keine Kamera, Zugriff verweigert), bleibt die
|
||||
// Eingabe von Hand. Die Ziffernfolge steht unter jedem Strichcode.
|
||||
"use strict";
|
||||
|
||||
import { decodeImage } from "../barcode.js";
|
||||
import { clear, el } from "../dom.js";
|
||||
|
||||
const SCAN_INTERVAL_MS = 120;
|
||||
|
||||
async function nativeDetector() {
|
||||
if (!("BarcodeDetector" in window)) return null;
|
||||
try {
|
||||
const formats = await window.BarcodeDetector.getSupportedFormats();
|
||||
const wanted = ["ean_13", "ean_8", "upc_a", "upc_e", "code_128", "qr_code"]
|
||||
.filter((f) => formats.includes(f));
|
||||
if (!wanted.length) return null;
|
||||
return new window.BarcodeDetector({ formats: wanted });
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet den Sucher als Überlagerung.
|
||||
* @returns {Promise<string|null>} erkannte Ziffernfolge oder null bei Abbruch
|
||||
*/
|
||||
export function scanBarcode() {
|
||||
return new Promise((resolve) => {
|
||||
let stream = null;
|
||||
let timer = null;
|
||||
let closed = false;
|
||||
|
||||
const video = el("video", {
|
||||
playsInline: true,
|
||||
muted: true,
|
||||
autoplay: true,
|
||||
className: "scan-video",
|
||||
});
|
||||
const canvas = document.createElement("canvas");
|
||||
const hint = el("p.scan-hint", {}, "Kamera wird geöffnet …");
|
||||
|
||||
const manualField = el("input", {
|
||||
type: "text",
|
||||
inputMode: "numeric",
|
||||
placeholder: "Ziffern unter dem Strichcode",
|
||||
maxLength: 32,
|
||||
onkeydown: (ev) => { if (ev.key === "Enter") useManual(); },
|
||||
});
|
||||
|
||||
function useManual() {
|
||||
const value = manualField.value.replace(/\D/g, "");
|
||||
if (value.length < 6) {
|
||||
hint.textContent = "Bitte mindestens sechs Ziffern eingeben.";
|
||||
return;
|
||||
}
|
||||
finish(value);
|
||||
}
|
||||
|
||||
const overlay = el("div.scan-overlay", {},
|
||||
el("div.scan-stage", {}, video, el("div.scan-frame", {})),
|
||||
hint,
|
||||
el("div.scan-manual", {},
|
||||
el("label", {}, "Oder von Hand eingeben"),
|
||||
el("div.row", {}, manualField,
|
||||
el("button.primary", { type: "button", onclick: useManual }, "Übernehmen"))),
|
||||
el("button.secondary.scan-close", { type: "button", onclick: () => finish(null) },
|
||||
"Abbrechen")
|
||||
);
|
||||
|
||||
function finish(code) {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
if (timer) clearInterval(timer);
|
||||
// Kamera zuverlässig freigeben - sonst leuchtet die Anzeigeleuchte
|
||||
// weiter und der Akku leidet.
|
||||
if (stream) for (const track of stream.getTracks()) track.stop();
|
||||
overlay.remove();
|
||||
document.body.classList.remove("scanning");
|
||||
window.removeEventListener("keydown", onKey);
|
||||
resolve(code);
|
||||
}
|
||||
|
||||
function onKey(ev) {
|
||||
if (ev.key === "Escape") finish(null);
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", onKey);
|
||||
document.body.classList.add("scanning");
|
||||
document.body.append(overlay);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
facingMode: { ideal: "environment" },
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 720 },
|
||||
},
|
||||
audio: false,
|
||||
});
|
||||
} catch (err) {
|
||||
hint.textContent =
|
||||
err.name === "NotAllowedError"
|
||||
? "Kein Zugriff auf die Kamera. Bitte in den Browsereinstellungen erlauben – oder die Ziffern von Hand eingeben."
|
||||
: "Keine Kamera verfügbar. Bitte die Ziffern von Hand eingeben.";
|
||||
manualField.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
video.srcObject = stream;
|
||||
try {
|
||||
await video.play();
|
||||
} catch {
|
||||
// Manche Browser verlangen eine Nutzeraktion; das Bild erscheint
|
||||
// dann trotzdem, sobald die Wiedergabe anläuft.
|
||||
}
|
||||
|
||||
const detector = await nativeDetector();
|
||||
hint.textContent = detector
|
||||
? "Strichcode in den Rahmen halten."
|
||||
: "Strichcode in den Rahmen halten. Für eine gute Erkennung waagerecht und gut ausgeleuchtet.";
|
||||
|
||||
timer = setInterval(async () => {
|
||||
if (closed || video.readyState < 2) return;
|
||||
|
||||
const width = video.videoWidth;
|
||||
const height = video.videoHeight;
|
||||
if (!width || !height) return;
|
||||
|
||||
if (detector) {
|
||||
try {
|
||||
const found = await detector.detect(video);
|
||||
if (found.length) {
|
||||
const value = String(found[0].rawValue || "").trim();
|
||||
if (value) return finish(value);
|
||||
}
|
||||
} catch {
|
||||
// Weiter mit dem eigenen Decoder.
|
||||
}
|
||||
}
|
||||
|
||||
// Nur den mittleren Streifen auswerten: Dort liegt der Code,
|
||||
// und es spart die meiste Rechenzeit.
|
||||
const bandHeight = Math.round(height * 0.35);
|
||||
const y = Math.round((height - bandHeight) / 2);
|
||||
canvas.width = width;
|
||||
canvas.height = bandHeight;
|
||||
|
||||
const context = canvas.getContext("2d", { willReadFrequently: true });
|
||||
context.drawImage(video, 0, y, width, bandHeight, 0, 0, width, bandHeight);
|
||||
|
||||
const code = decodeImage(context.getImageData(0, 0, width, bandHeight));
|
||||
if (code) finish(code);
|
||||
}, SCAN_INTERVAL_MS);
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
/** Ob ein Sucher überhaupt sinnvoll ist. Über reines HTTP verweigert der
|
||||
* Browser den Kamerazugriff - dann direkt zur Eingabe von Hand. */
|
||||
export function cameraAvailable() {
|
||||
return Boolean(navigator.mediaDevices?.getUserMedia) && window.isSecureContext;
|
||||
}
|
||||
Reference in New Issue
Block a user