diff --git a/ROADMAP.md b/ROADMAP.md
index 4c6c6f5..6d1d2ba 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -31,6 +31,26 @@ Bericht-Moduls.
---
+## 10. Kamera vernünftig + offene Handy-Prüfungen im Emulator (Eddy 19.10.2026: „mach das vernünftig")
+
+Anlass: Nach der Blitz/Fokus-Gegenprobe (Punkt 9) meldet Chrome außerdem Zoom 1–10× und Belichtungskorrektur ±3.
+Der Emulator läuft jetzt (SVM an) — die bisher „nur am Handy" prüfbaren Punkte gehen dort mit echtem Android-Chrome.
+
+- [x] ✅ 19.10.2026 **Zoom mit zwei Fingern im Sucher** (`applyConstraints({advanced:[{zoom}]})`, nur wenn `caps.zoom`
+ gemeldet) + Anzeige „1×"/„2,5×" unten im Sucher (gelb, sobald gezoomt), Antippen der Anzeige → zurück auf 1×.
+ Während der Geste immer nur ein Aufruf an die Kamera, Zwischenwerte verfallen; Ende der Geste löst keinen
+ Fokus-Tipp aus. Im Emulator mit **echter Zwei-Finger-Geste** (Emulator-Konsole → Touchscreen, siehe unten) geprüft:
+ Fingerabstand ×2,87 → 2,9×, zusammen → 1×, kein Fokus-Aufruf/Klick während der Gesten; Foto bei 2,5× zeigt genau
+ den Sucher-Ausschnitt. Chrome 153 zoomt über `control.zoomRatio` (nicht Ausschnitt) — die Fokus-/Belichtungs-
+ bereiche gelten dann laut AOSP-Definition relativ zum gezoomten Bild, die Tippstelle passt also auch im Zoom.
+- [x] ✅ Belichtung: **kein eigener Regler** — Antippen setzt bei Chrome Fokus-, Belichtungs- und Weißabgleich-Bereich
+ gemeinsam; im Emulator gemessen: `aeRegions` = `afRegions` = `awbRegions` = Tippstelle
+ → Display/Typenschild antippen = dort scharf UND richtig belichtet.
+- [ ] Emulator: echte Kamera + „Fertig" im Funkloch (Fotos gesichert, nach Netz-Rückkehr hochgeladen, Server zählt genau +n)
+- [ ] Emulator: Hochladen bei ausgeschaltetem Display (Background Sync)
+- [ ] Emulator: 8–10 Fotos auf einmal aus der Galerie (Fix aus Punkt 7)
+- [ ] Emulator: Zwei-Finger-Zoom im PDF-Betrachter mit echtem Mehrfinger-Touch (statt CDP-Nachbildung)
+
## 8. Knopf „Geteiltes ablegen" bleibt dauerhaft stehen (gemeldet 2026-10-19)
Eddy: *„bei der App Baustelle ist permanent ein Button relativ weit unten angezeigt über den aufgelisteten
diff --git a/app.css b/app.css
index e89590c..128cb4e 100644
--- a/app.css
+++ b/app.css
@@ -1219,6 +1219,7 @@ body {
align-items: center;
justify-content: center;
overflow: hidden;
+ touch-action: none; /* zwei Finger zoomen die Kamera, nicht die Seite */
}
/* contain: die Vorschau zeigt exakt das, was gespeichert wird (wichtig für Doku-Fotos) */
.camera-modal #cam-video {
@@ -1329,6 +1330,24 @@ body {
.camera-modal .cam-fokus-ring.aktiv { animation: cam-fokus 0.35s ease-out; }
.camera-modal .cam-fokus-ring.ok { border-color: #ffd34d; }
@keyframes cam-fokus { from { transform: scale(1.5); opacity: 0.4; } to { transform: scale(1); opacity: 0.9; } }
+/* Zoomstufe unten mittig im Sucher; Antippen = zurück auf 1× */
+.camera-modal .cam-zoom {
+ position: absolute;
+ bottom: 12px;
+ left: 50%;
+ transform: translateX(-50%);
+ min-width: 44px;
+ height: 32px;
+ padding: 0 10px;
+ border-radius: 16px;
+ border: 1px solid rgba(255, 255, 255, 0.5);
+ background: rgba(0, 0, 0, 0.55);
+ color: #fff;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+}
+.camera-modal .cam-zoom.aktiv { color: #ffd34d; border-color: #ffd34d; }
.camera-modal [hidden] { display: none !important; }
/* ============================================================
diff --git a/app.js b/app.js
index e09387e..07f1f3e 100644
--- a/app.js
+++ b/app.js
@@ -3626,6 +3626,7 @@ async function openCameraModal(orderId) {
Kamera wird gestartet…
+
@@ -3645,6 +3646,7 @@ async function openCameraModal(orderId) {
const blitzBtn = modal.querySelector('#cam-blitz');
const fokusRing = modal.querySelector('#cam-fokus-ring');
const flipBtn = modal.querySelector('#cam-flip');
+ const zoomBtn = modal.querySelector('#cam-zoom');
let stream = null;
let facing = 'environment';
@@ -3791,6 +3793,10 @@ async function openCameraModal(orderId) {
* → der Autofokus stellt laufend auf diese Stelle scharf. 'single-shot' meldet Chrome zwar,
* loest es auf Android aber nicht ein (kein AF-Trigger in VideoCaptureCamera2.java, der
* Modus bleibt CONTINUOUS_PICTURE — im Emulator per dumpsys gemessen, 19.10.2026).
+ * Chrome setzt mit dem Punkt zugleich Belichtungs- und Weissabgleich-Bereich (AE/AWB_REGIONS):
+ * Display oder Typenschild antippen = dort auch richtig belichtet, kein eigener Regler noetig.
+ * - Zoom mit zwei Fingern: track.getCapabilities().zoom, Chrome schneidet den Sensor aus
+ * (SCALER_CROP_REGION, gilt auch fuer takePhoto). Beim Zoomen verwirft Chrome den Tippbereich.
* Frontkamera, Webcam am Tablet (X12) usw. melden meist nichts davon → kein Knopf, kein Antippen.
*/
const BLITZ_TEXT = { off: '⚡ Aus', auto: '⚡ Auto', flash: '⚡ An', torch: '🔦 Licht' };
@@ -3800,6 +3806,8 @@ async function openCameraModal(orderId) {
let blitz = 'off';
let fotoBlitz = false; // kann takePhoto() blitzen (fillLightMode)?
let fokusModus = ''; // 'continuous' | 'single-shot' | '' (kein Antippen)
+ let zoomBereich = null; // { min, max, step } aus getCapabilities, sonst kein Zoom
+ let zoom = 1;
let tippHinweis = false;
function videoTrack() {
@@ -3807,8 +3815,8 @@ async function openCameraModal(orderId) {
}
async function kameraExtras(my) {
- imageCap = null; blitzModi = []; fotoBlitz = false; fokusModus = '';
- blitzBtn.hidden = true; fokusRing.hidden = true;
+ imageCap = null; blitzModi = []; fotoBlitz = false; fokusModus = ''; zoomBereich = null; zoom = 1;
+ blitzBtn.hidden = true; fokusRing.hidden = true; zoomBtn.hidden = true;
const track = videoTrack();
if (!track) return;
let caps = {};
@@ -3841,14 +3849,70 @@ async function openCameraModal(orderId) {
const fm = Array.isArray(caps.focusMode) ? caps.focusMode : [];
fokusModus = fm.includes('continuous') ? 'continuous' : (fm.includes('single-shot') ? 'single-shot' : '');
- if (fokusModus && !tippHinweis) {
+
+ const zb = caps.zoom;
+ if (zb && typeof zb.max === 'number' && zb.max > (zb.min || 1)) {
+ zoomBereich = { min: zb.min || 1, max: zb.max, step: zb.step || 0.1 };
+ try { zoom = track.getSettings().zoom || zoomBereich.min; } catch (_) { zoom = zoomBereich.min; }
+ zoomAnzeigen();
+ zoomBtn.hidden = false;
+ }
+
+ const tipps = [];
+ if (fokusModus) tipps.push('Antippen stellt dort scharf');
+ if (zoomBereich) tipps.push('zwei Finger zoomen');
+ if (tipps.length && !tippHinweis) {
tippHinweis = true;
- hint.textContent = 'Antippen stellt dort scharf';
+ const text = tipps.join(' · ');
+ hint.textContent = text;
hint.style.display = '';
- setTimeout(() => { if (hint.textContent === 'Antippen stellt dort scharf') hint.style.display = 'none'; }, 2500);
+ setTimeout(() => { if (hint.textContent === text) hint.style.display = 'none'; }, 2500);
}
}
+ function zoomAnzeigen() {
+ const z = Math.round(zoom * 10) / 10;
+ zoomBtn.textContent = (Number.isInteger(z) ? String(z) : z.toFixed(1).replace('.', ',')) + '×';
+ zoomBtn.classList.toggle('aktiv', z > (zoomBereich ? zoomBereich.min : 1));
+ }
+
+ // Zoom setzen: waehrend der Geste kommen viele Werte — immer nur einen Aufruf gleichzeitig
+ // an die Kamera schicken, danach den zuletzt gewuenschten (Zwischenwerte verfallen).
+ let zoomLaeuft = false, zoomZiel = null;
+ async function zoomSetzen(z) {
+ if (!zoomBereich) return;
+ const { min, max, step } = zoomBereich;
+ zoomZiel = Math.min(max, Math.max(min, Math.round(Math.round(z / step) * step * 100) / 100));
+ zoom = zoomZiel;
+ zoomAnzeigen();
+ if (zoomLaeuft) return;
+ zoomLaeuft = true;
+ while (zoomZiel !== null) {
+ const ziel = zoomZiel; zoomZiel = null;
+ const track = videoTrack();
+ if (!track) break;
+ try { await track.applyConstraints({ advanced: [{ zoom: ziel }] }); } catch (_) {}
+ }
+ zoomLaeuft = false;
+ }
+
+ zoomBtn.onclick = (ev) => { ev.stopPropagation(); if (zoomBereich) zoomSetzen(zoomBereich.min); };
+
+ // Zwei Finger auf dem Sucher = zoomen (Abstand der Finger relativ zum Start)
+ let pinch = null, pinchEnde = 0;
+ const fingerAbstand = (t) => Math.hypot(t[0].clientX - t[1].clientX, t[0].clientY - t[1].clientY);
+ stage.addEventListener('touchstart', (e) => {
+ if (e.touches.length === 2 && zoomBereich) pinch = { d0: fingerAbstand(e.touches), z0: zoom };
+ }, { passive: true });
+ stage.addEventListener('touchmove', (e) => {
+ if (!pinch || e.touches.length !== 2) return;
+ e.preventDefault();
+ zoomSetzen(pinch.z0 * fingerAbstand(e.touches) / pinch.d0);
+ }, { passive: false });
+ const pinchAus = (e) => { if (pinch && (!e.touches || e.touches.length < 2)) { pinch = null; pinchEnde = Date.now(); } };
+ stage.addEventListener('touchend', pinchAus);
+ stage.addEventListener('touchcancel', pinchAus);
+
async function lichtSchalten(an) {
const track = videoTrack();
if (!track) return false;
@@ -3893,6 +3957,7 @@ async function openCameraModal(orderId) {
stage.addEventListener('click', async (ev) => {
const track = videoTrack();
if (!track || !fokusModus || !video.videoWidth) return;
+ if (pinch || Date.now() - pinchEnde < 400) return; // Ende einer Zoom-Geste ist kein Tipp
// Antippstelle in Bildkoordinaten 0..1 — die Vorschau ist eingepasst (object-fit: contain)
const r = video.getBoundingClientRect();
const sk = Math.min(r.width / video.videoWidth, r.height / video.videoHeight);