Funktionswahl vor der Kamera, Wechsel und Knopf-Sichtbarkeit je Funktion
Neuer Vollbild-Dialog (src/ui/mode-dialog.js) mit drei grossen Flaechen, gebaut nach dem Muster der bestehenden Dialoge. Ablauf beim Start: erst die Fortsetzen-Frage (falls eine gesicherte Sitzung vorliegt), dann die Funktionswahl, erst danach startet die Kamera. Die gewaehlte Funktion steht antippbar in der Scan-Ansicht und laesst sich dort jederzeit wechseln - der Wechsel gilt sofort: laufende Suche startet/stoppt, "Modul scannen" erscheint nur noch in Funktion "Text erkennen", der Zielrahmen wird beim Wechsel weg von laufender Suche zurueckgesetzt. pipeline.js bleibt unangetastet; main.js reicht je nach Funktion nur unterschiedliche Adapter hinein (scan-recognition.js).
This commit is contained in:
+93
-17
@@ -5,6 +5,8 @@ import { decodeBarcodes } from './barcode.js';
|
|||||||
import { runOcr, isOcrAvailable } from './ocr.js';
|
import { runOcr, isOcrAvailable } from './ocr.js';
|
||||||
import { recognize } from './pipeline.js';
|
import { recognize } from './pipeline.js';
|
||||||
import { normalizeToken } from './spec.js';
|
import { normalizeToken } from './spec.js';
|
||||||
|
import { getScanMode } from './scan-modes.js';
|
||||||
|
import { buildRecognitionAdapters } from './scan-recognition.js';
|
||||||
import {
|
import {
|
||||||
createSession, proposeAssignment, commitAssignment,
|
createSession, proposeAssignment, commitAssignment,
|
||||||
undoLast, moveEntry, removeEntry, nextStackId,
|
undoLast, moveEntry, removeEntry, nextStackId,
|
||||||
@@ -14,6 +16,7 @@ import { renderScanView } from './ui/scan-view.js';
|
|||||||
import { showResult } from './ui/result-overlay.js';
|
import { showResult } from './ui/result-overlay.js';
|
||||||
import { askForStack } from './ui/ambiguous-dialog.js';
|
import { askForStack } from './ui/ambiguous-dialog.js';
|
||||||
import { askResumeSession } from './ui/resume-dialog.js';
|
import { askResumeSession } from './ui/resume-dialog.js';
|
||||||
|
import { askForScanMode } from './ui/mode-dialog.js';
|
||||||
import { renderSessionList } from './ui/session-list.js';
|
import { renderSessionList } from './ui/session-list.js';
|
||||||
import { describeSpec } from './ui/describe-spec.js';
|
import { describeSpec } from './ui/describe-spec.js';
|
||||||
|
|
||||||
@@ -38,6 +41,14 @@ let busy = false;
|
|||||||
// Scan-Durchlaufs ueberschrieben werden.
|
// Scan-Durchlaufs ueberschrieben werden.
|
||||||
let cameraUnavailableMessage = null;
|
let cameraUnavailableMessage = null;
|
||||||
|
|
||||||
|
// Vor dem ersten Start der Kamera waehlt der Nutzer eine der drei Scan-
|
||||||
|
// Funktionen (siehe scan-modes.js); erst danach steht diese Kennung fest.
|
||||||
|
// Waehrend applyMode() noch nicht gelaufen ist, darf weder die Dauersuche
|
||||||
|
// noch ein Scan-Durchlauf ausgeloest werden - beide sind bis dahin durch das
|
||||||
|
// Auswahl-Overlay (searchPaused()) bzw. durch busy/den fehlenden Kamerastart
|
||||||
|
// ohnehin unerreichbar.
|
||||||
|
let currentModeId = null;
|
||||||
|
|
||||||
const view = renderScanView(app, {
|
const view = renderScanView(app, {
|
||||||
onCapture: () => processCapture(() => grabFrame(view.video)),
|
onCapture: () => processCapture(() => grabFrame(view.video)),
|
||||||
onUndo: () => {
|
onUndo: () => {
|
||||||
@@ -56,8 +67,44 @@ const view = renderScanView(app, {
|
|||||||
openSessionList();
|
openSessionList();
|
||||||
},
|
},
|
||||||
onPickFile: (file) => processCapture(() => imageDataFromFile(file)),
|
onPickFile: (file) => processCapture(() => imageDataFromFile(file)),
|
||||||
|
onChangeMode: () => {
|
||||||
|
// Waehrend eine Erkennung laeuft, bleibt auch der Funktionswechsel
|
||||||
|
// unerreichbar - aus demselben Grund wie bei Rueckgaengig/Sitzungsliste.
|
||||||
|
if (busy) return;
|
||||||
|
openModeDialog();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uebernimmt eine (neu gewaehlte) Scan-Funktion: Anzeige, Sichtbarkeit von
|
||||||
|
* "Modul scannen" und - bei einem Wechsel weg von laufender Codesuche - der
|
||||||
|
* Zielrahmen wird sofort zurueckgesetzt, statt auf den naechsten Durchlauf
|
||||||
|
* der Dauersuche zu warten.
|
||||||
|
* @param {string} modeId
|
||||||
|
*/
|
||||||
|
function applyMode(modeId) {
|
||||||
|
currentModeId = modeId;
|
||||||
|
const mode = getScanMode(modeId);
|
||||||
|
view.setMode(mode.label);
|
||||||
|
// "Modul scannen" hat nur dort etwas zu tun, wo es keine laufende Suche
|
||||||
|
// gibt (Funktion 3) - siehe Kommentar in scan-view.js/setCaptureVisible.
|
||||||
|
view.setCaptureVisible(!mode.continuousSearch);
|
||||||
|
if (!mode.continuousSearch) {
|
||||||
|
view.setFrameDetected(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Oeffnet die Funktionswahl (auch fuer den Wechsel aus der laufenden
|
||||||
|
* Scan-Ansicht heraus, nicht nur beim Start). Solange das Overlay offen ist,
|
||||||
|
* pausiert die Dauersuche von selbst (siehe searchPaused()).
|
||||||
|
*/
|
||||||
|
async function openModeDialog() {
|
||||||
|
closeOverlays();
|
||||||
|
const modeId = await askForScanMode(app, { current: currentModeId });
|
||||||
|
applyMode(modeId);
|
||||||
|
}
|
||||||
|
|
||||||
/** Spiegelt den Sitzungsstand in die Oberflaeche und sichert ihn gegen Neuladen ab. */
|
/** Spiegelt den Sitzungsstand in die Oberflaeche und sichert ihn gegen Neuladen ab. */
|
||||||
function syncView() {
|
function syncView() {
|
||||||
view.setStacks(session.stacks);
|
view.setStacks(session.stacks);
|
||||||
@@ -98,9 +145,14 @@ async function processCapture(getFrame) {
|
|||||||
(stack) => Array.isArray(stack.codes) && stack.codes.includes(normalized),
|
(stack) => Array.isArray(stack.codes) && stack.codes.includes(normalized),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
// Welcher Adapter tatsaechlich etwas tut, haengt von der gewaehlten
|
||||||
|
// Scan-Funktion ab (siehe scan-modes.js/scan-recognition.js) - recognize()
|
||||||
|
// selbst (pipeline.js) bleibt unveraendert und kennt keine Funktionen.
|
||||||
|
const { decodeBarcodes: decode, runOcr: recognizeText } =
|
||||||
|
buildRecognitionAdapters(currentModeId, { decodeBarcodes, runOcr });
|
||||||
const { spec, source, confidence, codes } = await recognize(
|
const { spec, source, confidence, codes } = await recognize(
|
||||||
frame,
|
frame,
|
||||||
{ decodeBarcodes, runOcr, isKnownCode },
|
{ decodeBarcodes: decode, runOcr: recognizeText, isKnownCode },
|
||||||
);
|
);
|
||||||
|
|
||||||
const plan = proposeAssignment(targetSession, spec, codes);
|
const plan = proposeAssignment(targetSession, spec, codes);
|
||||||
@@ -218,6 +270,11 @@ function searchPaused() {
|
|||||||
* nichts tun.
|
* nichts tun.
|
||||||
*/
|
*/
|
||||||
async function runSearchAttempt() {
|
async function runSearchAttempt() {
|
||||||
|
// Funktion 3 (Text erkennen) hat keine laufende Suche - siehe scan-modes.js.
|
||||||
|
// Ein Wechsel dorthin setzt den Rahmen bereits in applyMode() zurueck;
|
||||||
|
// hier genuegt es, schlicht nichts zu tun.
|
||||||
|
if (!getScanMode(currentModeId).continuousSearch) return;
|
||||||
|
|
||||||
if (searchPaused()) {
|
if (searchPaused()) {
|
||||||
view.setFrameDetected(false);
|
view.setFrameDetected(false);
|
||||||
return;
|
return;
|
||||||
@@ -235,7 +292,7 @@ async function runSearchAttempt() {
|
|||||||
|
|
||||||
let codes;
|
let codes;
|
||||||
try {
|
try {
|
||||||
codes = await decodeBarcodes(region);
|
codes = await decodeBarcodes(region, getScanMode(currentModeId).barcodeFormats);
|
||||||
} catch {
|
} catch {
|
||||||
view.setFrameDetected(false);
|
view.setFrameDetected(false);
|
||||||
return;
|
return;
|
||||||
@@ -331,20 +388,25 @@ function openSessionList() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
startCamera(view.video)
|
/** Startet die Kamera und, sobald sie brauchbare Bilder liefert, die Dauersuche. */
|
||||||
.then(() => {
|
function startCameraAndSearch() {
|
||||||
// Erst ab hier liefert die Kamera brauchbare Bilder - die Dauersuche
|
startCamera(view.video)
|
||||||
// laeuft fortan im Hintergrund, ohne dass der Nutzer etwas tun muss.
|
.then(() => {
|
||||||
continuousSearchLoop();
|
// Erst ab hier liefert die Kamera brauchbare Bilder - die Dauersuche
|
||||||
})
|
// laeuft fortan im Hintergrund, ohne dass der Nutzer etwas tun muss.
|
||||||
.catch((error) => {
|
// runSearchAttempt() selbst haelt sich zurueck, wenn die gewaehlte
|
||||||
// Kein automatischer Aufruf der Dateiauswahl ohne Nutzergeste - Handy-
|
// Funktion keine laufende Suche vorsieht (Funktion 3).
|
||||||
// Browser blockieren das regelmaessig. Stattdessen bleibt der Ersatzweg
|
continuousSearchLoop();
|
||||||
// ueber eine sichtbare, dauerhaft eingeblendete Schaltflaeche erreichbar.
|
})
|
||||||
cameraUnavailableMessage = `Kamera nicht verfuegbar (${error.message}) — Bild auswaehlen`;
|
.catch((error) => {
|
||||||
view.setStatus(cameraUnavailableMessage, true);
|
// Kein automatischer Aufruf der Dateiauswahl ohne Nutzergeste - Handy-
|
||||||
view.setFilePickerVisible(true);
|
// Browser blockieren das regelmaessig. Stattdessen bleibt der Ersatzweg
|
||||||
});
|
// ueber eine sichtbare, dauerhaft eingeblendete Schaltflaeche erreichbar.
|
||||||
|
cameraUnavailableMessage = `Kamera nicht verfuegbar (${error.message}) — Bild auswaehlen`;
|
||||||
|
view.setStatus(cameraUnavailableMessage, true);
|
||||||
|
view.setFilePickerVisible(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bietet eine beim Start gefundene, gesicherte Sitzung zur Fortsetzung an,
|
* Bietet eine beim Start gefundene, gesicherte Sitzung zur Fortsetzung an,
|
||||||
@@ -367,4 +429,18 @@ async function initResumeOffer() {
|
|||||||
syncView();
|
syncView();
|
||||||
}
|
}
|
||||||
|
|
||||||
initResumeOffer();
|
/**
|
||||||
|
* Ablauf beim Start: erst - falls vorhanden - die Frage nach dem Fortsetzen
|
||||||
|
* einer gesicherten Sitzung, danach die Wahl der Scan-Funktion, und erst
|
||||||
|
* danach die Kamera. So sieht der Nutzer die Funktionswahl nie, bevor die
|
||||||
|
* Fortsetzen-Frage (falls noetig) beantwortet ist, und die Kamera startet
|
||||||
|
* nie, bevor feststeht, welche Codearten sie ueberhaupt suchen soll.
|
||||||
|
*/
|
||||||
|
async function boot() {
|
||||||
|
await initResumeOffer();
|
||||||
|
const modeId = await askForScanMode(app);
|
||||||
|
applyMode(modeId);
|
||||||
|
startCameraAndSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
boot();
|
||||||
|
|||||||
@@ -21,6 +21,19 @@ body {
|
|||||||
|
|
||||||
#app { display: flex; flex-direction: column; height: 100dvh; }
|
#app { display: flex; flex-direction: column; height: 100dvh; }
|
||||||
|
|
||||||
|
.mode-button {
|
||||||
|
min-height: 56px;
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
background: #1c2027;
|
||||||
|
color: var(--fg);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: left;
|
||||||
|
padding: 0 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.camera { position: relative; flex: 1; overflow: hidden; background: #000; }
|
.camera { position: relative; flex: 1; overflow: hidden; background: #000; }
|
||||||
.camera video { width: 100%; height: 100%; object-fit: cover; }
|
.camera video { width: 100%; height: 100%; object-fit: cover; }
|
||||||
|
|
||||||
@@ -115,5 +128,22 @@ body {
|
|||||||
.choices { display: grid; gap: 12px; width: 100%; max-width: 420px; }
|
.choices { display: grid; gap: 12px; width: 100%; max-width: 420px; }
|
||||||
.choices button { min-height: 64px; font-size: 20px; }
|
.choices button { min-height: 64px; font-size: 20px; }
|
||||||
|
|
||||||
|
/* Auswahl der Scan-Funktion vor dem Scannen (mode-dialog.js): drei grosse,
|
||||||
|
gleich gestaltete Flaechen statt schmaler Schaltflaechen. */
|
||||||
|
.mode-tile {
|
||||||
|
min-height: 96px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-tile-label { font-size: 24px; font-weight: 700; }
|
||||||
|
.mode-tile-hint { font-size: 14px; opacity: 0.8; }
|
||||||
|
|
||||||
|
.mode-tile-current { outline: 3px solid var(--fg); outline-offset: -3px; }
|
||||||
|
|
||||||
.status { padding: 8px 12px; font-size: 14px; color: var(--muted); }
|
.status { padding: 8px 12px; font-size: 14px; color: var(--muted); }
|
||||||
.status.warn { color: #f0b429; }
|
.status.warn { color: #f0b429; }
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { SCAN_MODES } from '../scan-modes.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kurzer Hinweis je Funktion, direkt aus ihren Merkmalen abgeleitet (siehe
|
||||||
|
* scan-modes.js) - keine zweite Stelle mit eigenem Text je Funktion.
|
||||||
|
*/
|
||||||
|
function describeMode(mode) {
|
||||||
|
if (mode.useOcr) return 'Auf Knopfdruck';
|
||||||
|
return 'Läuft von selbst, sobald ein Code im Bild ist';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vollbild-Auswahl der Scan-Funktion: drei grosse Flaechen, eine je Funktion
|
||||||
|
* aus scan-modes.js (einzige Quelle fuer Kennung und Beschriftung). Gebaut
|
||||||
|
* nach demselben Muster wie die anderen Vollbild-Dialoge (ambiguous-dialog.js,
|
||||||
|
* resume-dialog.js): Geschwister waehrend der Anzeige per inert unerreichbar,
|
||||||
|
* Fokus kehrt danach zurueck. Reine Darstellung - kennt weder Kamera noch
|
||||||
|
* Erkennung.
|
||||||
|
*
|
||||||
|
* Wird sowohl vor dem ersten Start der Kamera verwendet als auch fuer den
|
||||||
|
* Wechsel aus der laufenden Scan-Ansicht heraus (siehe main.js).
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} root
|
||||||
|
* @param {{current?: string}} [args] Kennung der aktuell gewaehlten Funktion,
|
||||||
|
* falls es sich um einen Wechsel handelt - hebt die passende Flaeche hervor.
|
||||||
|
* @returns {Promise<string>} Kennung der gewaehlten Funktion (siehe scan-modes.js).
|
||||||
|
*/
|
||||||
|
export function askForScanMode(root, { current } = {}) {
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'overlay';
|
||||||
|
overlay.style.background = 'var(--bg)';
|
||||||
|
overlay.setAttribute('role', 'dialog');
|
||||||
|
overlay.setAttribute('aria-modal', 'true');
|
||||||
|
overlay.setAttribute('aria-labelledby', 'mode-dialog-heading');
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<div class="stack-id" id="mode-dialog-heading" style="font-size:28px">Funktion wählen</div>
|
||||||
|
<div class="choices mode-choices" id="choices"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const choices = overlay.querySelector('#choices');
|
||||||
|
|
||||||
|
// Siehe ambiguous-dialog.js / resume-dialog.js: waehrend der Dialog offen
|
||||||
|
// ist, werden die Geschwisterelemente per inert unerreichbar gemacht und
|
||||||
|
// danach wieder freigegeben.
|
||||||
|
const previouslyFocused = document.activeElement;
|
||||||
|
const madeInert = [];
|
||||||
|
for (const sibling of root.children) {
|
||||||
|
if (!sibling.hasAttribute('inert')) {
|
||||||
|
sibling.setAttribute('inert', '');
|
||||||
|
madeInert.push(sibling);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root.appendChild(overlay);
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let settled = false;
|
||||||
|
const finish = (modeId) => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
overlay.remove();
|
||||||
|
for (const sibling of madeInert) {
|
||||||
|
sibling.removeAttribute('inert');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
previouslyFocused &&
|
||||||
|
typeof previouslyFocused.focus === 'function' &&
|
||||||
|
document.contains(previouslyFocused)
|
||||||
|
) {
|
||||||
|
previouslyFocused.focus();
|
||||||
|
} else {
|
||||||
|
root.focus?.();
|
||||||
|
}
|
||||||
|
resolve(modeId);
|
||||||
|
};
|
||||||
|
|
||||||
|
let firstButton = null;
|
||||||
|
for (const mode of SCAN_MODES) {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.className = 'action mode-tile';
|
||||||
|
if (mode.id === current) button.classList.add('mode-tile-current');
|
||||||
|
button.setAttribute('aria-pressed', String(mode.id === current));
|
||||||
|
button.innerHTML = `
|
||||||
|
<span class="mode-tile-label">${mode.label}</span>
|
||||||
|
<span class="mode-tile-hint"></span>
|
||||||
|
`;
|
||||||
|
button.querySelector('.mode-tile-hint').textContent = describeMode(mode);
|
||||||
|
button.addEventListener('click', () => finish(mode.id));
|
||||||
|
choices.appendChild(button);
|
||||||
|
firstButton = firstButton ?? button;
|
||||||
|
}
|
||||||
|
|
||||||
|
firstButton?.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
+29
-3
@@ -4,14 +4,15 @@ import { FRAME_INSET } from '../camera.js';
|
|||||||
* Baut die Hauptansicht auf und liefert Aktualisierungsfunktionen zurueck.
|
* Baut die Hauptansicht auf und liefert Aktualisierungsfunktionen zurueck.
|
||||||
* Kennt weder Kamera noch Erkennung - alles kommt ueber die Rueckrufe.
|
* Kennt weder Kamera noch Erkennung - alles kommt ueber die Rueckrufe.
|
||||||
*/
|
*/
|
||||||
export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile }) {
|
export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile, onChangeMode }) {
|
||||||
root.innerHTML = `
|
root.innerHTML = `
|
||||||
|
<button class="mode-button" id="mode-button" type="button"></button>
|
||||||
<div class="camera">
|
<div class="camera">
|
||||||
<video id="preview" playsinline muted></video>
|
<video id="preview" playsinline muted></video>
|
||||||
<div class="frame"></div>
|
<div class="frame"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="status" id="status"></div>
|
<div class="status" id="status"></div>
|
||||||
<button class="action capture" id="capture">Modul scannen</button>
|
<button class="action capture" id="capture" hidden>Modul scannen</button>
|
||||||
<button class="action secondary" id="pick-file-button" hidden>Bild auswählen</button>
|
<button class="action secondary" id="pick-file-button" hidden>Bild auswählen</button>
|
||||||
<div class="stacks" id="stacks"></div>
|
<div class="stacks" id="stacks"></div>
|
||||||
<div class="last">
|
<div class="last">
|
||||||
@@ -27,6 +28,8 @@ export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile
|
|||||||
const fileEl = root.querySelector('#file');
|
const fileEl = root.querySelector('#file');
|
||||||
const pickFileButtonEl = root.querySelector('#pick-file-button');
|
const pickFileButtonEl = root.querySelector('#pick-file-button');
|
||||||
const frameEl = root.querySelector('.frame');
|
const frameEl = root.querySelector('.frame');
|
||||||
|
const captureEl = root.querySelector('#capture');
|
||||||
|
const modeButtonEl = root.querySelector('#mode-button');
|
||||||
|
|
||||||
// FRAME_INSET (camera.js) ist die einzige Quelle der Rahmenmasse - sowohl
|
// FRAME_INSET (camera.js) ist die einzige Quelle der Rahmenmasse - sowohl
|
||||||
// fuer diesen sichtbaren Rahmen als auch fuer den nativen Ausschnitt
|
// fuer diesen sichtbaren Rahmen als auch fuer den nativen Ausschnitt
|
||||||
@@ -36,8 +39,9 @@ export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile
|
|||||||
`${FRAME_INSET.top * 100}% ${FRAME_INSET.right * 100}% ` +
|
`${FRAME_INSET.top * 100}% ${FRAME_INSET.right * 100}% ` +
|
||||||
`${FRAME_INSET.bottom * 100}% ${FRAME_INSET.left * 100}%`;
|
`${FRAME_INSET.bottom * 100}% ${FRAME_INSET.left * 100}%`;
|
||||||
|
|
||||||
root.querySelector('#capture').addEventListener('click', onCapture);
|
captureEl.addEventListener('click', onCapture);
|
||||||
root.querySelector('#undo').addEventListener('click', onUndo);
|
root.querySelector('#undo').addEventListener('click', onUndo);
|
||||||
|
modeButtonEl.addEventListener('click', onChangeMode);
|
||||||
// Klick auf die sichtbare Schaltflaeche ist selbst die Nutzergeste, die
|
// Klick auf die sichtbare Schaltflaeche ist selbst die Nutzergeste, die
|
||||||
// die versteckte Dateiauswahl oeffnen darf - anders als ein Aufruf aus dem
|
// die versteckte Dateiauswahl oeffnen darf - anders als ein Aufruf aus dem
|
||||||
// Code heraus, den Handy-Browser blockieren.
|
// Code heraus, den Handy-Browser blockieren.
|
||||||
@@ -74,6 +78,28 @@ export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile
|
|||||||
lastEl.textContent = 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})`);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Modul scannen" hat nur in Funktionen mit Texterkennung auf Knopfdruck
|
||||||
|
* etwas zu tun (Funktion 3) - in Funktionen mit laufender Codesuche sucht
|
||||||
|
* die App ohnehin durchgehend, ein Knopf dafuer waere wirkungslos.
|
||||||
|
* @param {boolean} visible
|
||||||
|
*/
|
||||||
|
setCaptureVisible(visible) {
|
||||||
|
captureEl.hidden = !visible;
|
||||||
|
},
|
||||||
|
|
||||||
/** @param {string} text @param {boolean} warn */
|
/** @param {string} text @param {boolean} warn */
|
||||||
setStatus(text, warn = false) {
|
setStatus(text, warn = false) {
|
||||||
statusEl.textContent = text;
|
statusEl.textContent = text;
|
||||||
|
|||||||
Reference in New Issue
Block a user