Files
ocr_scanner/src/ui/scan-view.js
T
TanerUsluandClaude Opus 5 4b6bb065b1 Zählen: Aufnahme statt Dauerbetrieb
Die Zaehlanzeige sprang beim Nutzer im Dauerbetrieb wild zwischen etwa 3
und 60, obwohl dasselbe Verfahren an einem Standfoto stabil 18 lieferte -
eine Schaetzung wird durch staendiges Neuanzeigen nicht praeziser, nur
unruhig. "Zählen" laeuft jetzt wie "Text erkennen" auf Knopfdruck: eine
Aufnahme nimmt binnen rund einer Sekunde fuenf Bilder aus dem Zielrahmen
auf, zaehlt jedes einzeln (count-objects.js unveraendert) und zeigt deren
Median (neues Modul count-capture.js). Das Ergebnis bleibt stehen, bis
erneut angetippt wird; die Diagnosezeile zeigt zusaetzlich die fuenf
Einzelmessungen. Der gleitende Median ueber neun Durchlaeufe der
Dauerzaehlung (count-history.js) entfaellt, da die Aufnahme dieselbe
Robustheit bereits selbst leistet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 09:36:43 +02:00

223 lines
9.3 KiB
JavaScript

import { FRAME_INSET } from '../camera.js';
/**
* Baut die Hauptansicht auf und liefert Aktualisierungsfunktionen zurueck.
* Kennt weder Kamera noch Erkennung - alles kommt ueber die Rueckrufe.
*/
export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile, onChangeMode }) {
root.innerHTML = `
<button class="mode-button" id="mode-button" type="button"></button>
<div class="camera">
<video id="preview" playsinline muted></video>
<div class="frame"></div>
<div class="count-display" id="count-display" hidden>
<button class="count-number" id="count-number" type="button" aria-live="polite"
aria-label="Messwerte zur Zaehlung ein- oder ausblenden"></button>
<div class="count-diagnostics" id="count-diagnostics" hidden aria-live="polite"></div>
</div>
</div>
<div class="status" id="status"></div>
<button class="action capture" id="capture" hidden></button>
<button class="action secondary" id="pick-file-button" hidden>Bild auswählen</button>
<div class="stacks" id="stacks"></div>
<div class="last">
<span id="last">Noch nichts erfasst</span>
<button class="action secondary" id="undo" style="min-width:64px" aria-label="Letzten Scan zurücknehmen">&#8630;</button>
</div>
<input type="file" id="file" accept="image/*" hidden />
`;
const stacksEl = root.querySelector('#stacks');
const lastEl = root.querySelector('#last');
const statusEl = root.querySelector('#status');
const fileEl = root.querySelector('#file');
const pickFileButtonEl = root.querySelector('#pick-file-button');
const frameEl = root.querySelector('.frame');
const captureEl = root.querySelector('#capture');
const modeButtonEl = root.querySelector('#mode-button');
const countDisplayEl = root.querySelector('#count-display');
const countNumberEl = root.querySelector('#count-number');
const countDiagnosticsEl = root.querySelector('#count-diagnostics');
// Ob die Diagnosezeile unter der grossen Zahl gerade eingeblendet ist -
// rein lokaler Anzeigezustand dieser Ansicht, siehe setCountVisible()
// (Zuruecksetzen beim Verlassen der Funktion "Zählen") und den Klick-
// Handler auf die grosse Zahl unten (Umschalten).
let countDiagnosticsVisible = false;
// FRAME_INSET (camera.js) ist die einzige Quelle der Rahmenmasse - sowohl
// fuer diesen sichtbaren Rahmen als auch fuer den nativen Ausschnitt
// (grabFrameRegion). Damit koennen beide nicht mehr auseinanderlaufen;
// das Stilblatt selbst legt keinen Abstand mehr fest.
frameEl.style.inset =
`${FRAME_INSET.top * 100}% ${FRAME_INSET.right * 100}% ` +
`${FRAME_INSET.bottom * 100}% ${FRAME_INSET.left * 100}%`;
captureEl.addEventListener('click', onCapture);
root.querySelector('#undo').addEventListener('click', onUndo);
modeButtonEl.addEventListener('click', onChangeMode);
// Klick auf die sichtbare Schaltflaeche ist selbst die Nutzergeste, die
// die versteckte Dateiauswahl oeffnen darf - anders als ein Aufruf aus dem
// Code heraus, den Handy-Browser blockieren.
pickFileButtonEl.addEventListener('click', () => fileEl.click());
fileEl.addEventListener('change', () => {
if (fileEl.files[0]) onPickFile(fileEl.files[0]);
fileEl.value = '';
});
// Antippen der grossen Zahl blendet die Diagnosezeile ein, nochmaliges
// Antippen wieder aus (siehe Moduldoku zu setCount()/setCountDiagnostics()
// unten) - reiner Anzeigezustand dieser Ansicht, main.js muss davon
// nichts wissen. Die grosse Zahl selbst bleibt dabei an derselben Stelle
// stehen: die Diagnosezeile ist eigenstaendig positioniert (siehe
// styles.css), ihr Ein-/Ausblenden nimmt der Zahl nicht den Platz weg.
countNumberEl.addEventListener('click', () => {
countDiagnosticsVisible = !countDiagnosticsVisible;
countDiagnosticsEl.hidden = !countDiagnosticsVisible;
});
return {
video: root.querySelector('#preview'),
/** @param {{id: string, count: number}[]} stacks */
setStacks(stacks) {
stacksEl.innerHTML = '';
if (stacks.length === 0) {
// Auch ohne erfasste Stapel muss die Sitzungsliste erreichbar
// bleiben - darueber liegt der einzige Weg zu "Sitzung beenden".
const button = document.createElement('button');
button.textContent = 'Noch keine Stapel';
button.addEventListener('click', onOpenList);
stacksEl.appendChild(button);
return;
}
for (const stack of stacks) {
const button = document.createElement('button');
button.textContent = `${stack.id}: ${stack.count}`;
button.addEventListener('click', onOpenList);
stacksEl.appendChild(button);
}
},
setLast(text) {
lastEl.textContent = text;
},
/**
* Zeigt die gewaehlte Scan-Funktion in der Ansicht an - antippbar, um zu
* wechseln (siehe onChangeMode). Beschriftung kommt vom Aufrufer, der sie
* seinerseits aus scan-modes.js liest; diese Ansicht kennt die Funktionen
* selbst nicht.
* @param {string} label
*/
setMode(label) {
modeButtonEl.textContent = `${label} ändern ⌄`;
modeButtonEl.setAttribute('aria-label', `Scan-Funktion wechseln (aktuell: ${label})`);
},
/**
* Diese eine Schaltflaeche bedient beide Knopfdruck-Funktionen ("Modul
* scannen" in Funktion 3 "Text erkennen", "Zählen"/"Nochmal zählen" in
* Funktion 4 "Zählen") - nur ihr Wortlaut und ihr Rueckruf unterscheiden
* sich, siehe setCaptureLabel() unten und main.js/applyMode(). In
* Funktionen mit laufender Codesuche sucht die App ohnehin durchgehend,
* ein Knopf dafuer waere wirkungslos.
* @param {boolean} visible
*/
setCaptureVisible(visible) {
captureEl.hidden = !visible;
},
/**
* Setzt den Wortlaut der Aufnahme-Schaltflaeche - siehe setCaptureVisible()
* oben fuer den Grund, warum es dafuer nur eine gemeinsame Flaeche statt
* einer zweiten daneben gibt. main.js setzt hier "Modul scannen" (Funktion
* "Text erkennen"), "Zählen" (Funktion "Zählen", vor der ersten Aufnahme
* seit Betreten der Funktion) bzw. "Nochmal zählen" (danach).
* @param {string} text
*/
setCaptureLabel(text) {
captureEl.textContent = text;
},
/** @param {string} text @param {boolean} warn */
setStatus(text, warn = false) {
statusEl.textContent = text;
statusEl.classList.toggle('warn', warn);
},
openFilePicker() {
fileEl.click();
},
/**
* Blendet die Ersatzweg-Schaltflaeche "Bild auswaehlen" ein oder aus.
* Wird eingeblendet, wenn die Kamera nicht zur Verfuegung steht, und
* bleibt dann dauerhaft sichtbar.
* @param {boolean} visible
*/
setFilePickerVisible(visible) {
pickFileButtonEl.hidden = !visible;
},
/**
* Hebt den Zielrahmen hervor, solange die Dauersuche einen Barcode im
* Ausschnitt erkennt - schlichte Farbaenderung, keine Animation. Der
* Nutzer sieht damit beim Hinhalten sofort, ob es klappt.
* @param {boolean} detected
*/
setFrameDetected(detected) {
frameEl.classList.toggle('detected', detected);
},
/**
* Blendet die grosse Zaehlanzeige ein oder aus (nur in Funktion
* "Zählen" sichtbar, siehe applyMode() in main.js). Beim Ausblenden wird
* die zuletzt angezeigte Zahl geloescht, damit beim naechsten Wechsel in
* diese Funktion nicht kurz eine veraltete Zahl aufblitzt - ebenso die
* Diagnosezeile: sie faellt dabei wieder in ihren Ausgangszustand
* (verborgen, leer) zurueck, damit sie nach einem Funktionswechsel nicht
* ungefragt (weil sie beim letzten Mal offen war) oder mit veralteten
* Werten wieder auftaucht. Das gilt sowohl beim Verlassen der Funktion
* "Zählen" als auch beim erneuten Betreten - ein stehendes Ergebnis
* einer frueheren Aufnahme (andere Teile, andere Stelle) darf nicht
* hinueberwirken (siehe main.js/applyMode()).
* @param {boolean} visible
*/
setCountVisible(visible) {
countDisplayEl.hidden = !visible;
if (!visible) {
countNumberEl.textContent = '';
countDiagnosticsEl.textContent = '';
countDiagnosticsVisible = false;
countDiagnosticsEl.hidden = true;
}
},
/**
* Zeigt das Ergebnis der zuletzt angetippten Zaehl-Aufnahme (Median der
* fuenf Einzelmessungen dieser Aufnahme, siehe count-capture.js und
* runCountCapture() in main.js) - gross und aus Armlaenge lesbar. Bleibt
* unveraendert stehen, bis der Nutzer erneut auf die Schaltflaeche
* "Nochmal zählen" tippt (keine fortlaufende Aktualisierung mehr). Ein
* Antippen der Zahl selbst blendet die Diagnosezeile darunter ein oder
* aus (siehe Klick-Handler oben).
* @param {number} count
*/
setCount(count) {
countNumberEl.textContent = String(count);
},
/**
* Setzt den Text der kleinen Diagnosezeile unter der grossen Zahl (siehe
* main.js fuer den genauen Wortlaut, u. a. die fuenf Einzelmessungen der
* zuletzt angetippten Aufnahme) - unabhaengig davon, ob sie gerade
* eingeblendet ist. Reine Textaktualisierung, die Sichtbarkeit steuert
* ausschliesslich der Klick auf die grosse Zahl (siehe oben).
* @param {string} text
*/
setCountDiagnostics(text) {
countDiagnosticsEl.textContent = text;
},
};
}