Files
einkaufsapp/web/html/js/views/scanner.js
2026-08-08 20:31:58 +02:00

172 lines
5.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
}