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>
223 lines
9.3 KiB
JavaScript
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">↶</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;
|
|
},
|
|
};
|
|
}
|