diff --git a/README.md b/README.md index a31c7bc..ea32c84 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ Mobile Progressive Web App für die Baustellen-Doku — Foto-Upload, Sprach- und - ✅ **Neuer Auftrag** (FAB): Kunde per Suche/Zuletzt-verwendet wählen, dann **Ihr Zeichen** (Pflicht), **Auftragsbeschreibung** (optional), **Geplanter Liefertermin** (Datum + Uhrzeit) und Checkbox **Auftrag direkt freigeben**. Kunden-Defaults (Zahlungsbedingung/Zahlart, Ansprechpartner) werden übernommen. Bei „direkt freigeben" wird der Auftrag **ohne Position** validiert — die Leistungen kommen aus dem Stundenzettel. (Bis Bericht 1.3.1 hängte das Backend hier eine Std-Lohn-Zeile mit Menge 1 an; die verfälschte den Auftragswert, stand als offene Restmenge in der Lieferauflistung und im Kunden-PDF.) **Foto-Upload** -- ✅ Live-Kamera-Modal (getUserMedia): bleibt für Serien-Aufnahmen offen — Auslöser + Filmstreifen mit Upload-Status je Foto (💾 gesichert → ⏳ lädt hoch → ✓ fertig), Front-/Rückkamera-Umschaltung, Fehlschuss löschbar +- ✅ Live-Kamera-Modal (getUserMedia): bleibt für Serien-Aufnahmen offen — Auslöser + Filmstreifen mit Upload-Status je Foto (💾 gesichert → ⏳ lädt hoch → ✓ fertig), Front-/Rückkamera-Umschaltung (nur bei zwei Kameras), Fehlschuss löschbar - ✅ **Volle Kameraauflösung**: ausgelöst wird über `ImageCapture.takePhoto()` (native JPEG der Kamera inkl. EXIF) — ein Standbild aus dem Video-Stream hat nur Stream-Auflösung und ist nur noch die Rückfallebene (iOS/Safari, ältere Browser) -- ✅ **Blitz und Fokus** (19.10.2026): Knopf oben schaltet reihum Aus → Auto → An → 🔦 Licht (nur was das Gerät meldet, Auswahl bleibt gemerkt; Blitz wirkt über `takePhoto({fillLightMode})`). Antippen stellt an der Stelle scharf und hält den Fokus („🎯 Fokus fest · zurück auf Auto“). Frontkamera/Tablet-Webcam ohne diese Fähigkeiten: kein Knopf, Antippen ohne Wirkung +- ✅ **Blitz und Fokus** (19.10.2026): Knopf oben schaltet reihum Aus → Auto → An → 🔦 Licht (nur was das Gerät meldet, Auswahl bleibt gemerkt; Blitz wirkt über `takePhoto({fillLightMode})`). Antippen stellt den Autofokus auf diese Stelle (Tippstelle wird in Sensorlage zurückgedreht — Chrome tut das nicht; ein echtes „Fokus festhalten“ bietet Chrome auf Android nicht an). Frontkamera/Tablet-Webcam ohne diese Fähigkeiten: kein Knopf, Antippen ohne Wirkung. Knopf „Kamera wechseln“ (weißes Symbol) nur bei mindestens zwei Kameras. Im Android-Emulator per `dumpsys media.camera` gegengeprüft (ROADMAP Punkt 9) - ✅ **Verkleinerte Fotos heißen `.jpg`** und transparente Bilder bekommen beim Verkleinern weißen Grund (vorher behielt ein verkleinertes AVIF/WebP seinen Namen und wurde vom Server abgelehnt) - ✅ Alternativ Aufnahme aus der Galerie (Mehrfachauswahl: alle Dateien werden sofort nach der Auswahl eingelesen, erst dann hochgeladen, weil Android die Galerie-Dateien nur kurz lesbar hält, siehe ROADMAP Punkt 7) - ✅ **Fotoqualität im Admin einstellbar** (Bericht-Modul → Fotoqualität, `api/config.php`): Vorgabe ist **1:1, gar nicht verkleinern**; optional längste Seite + JPEG-Qualität. Gesichert wird immer zuerst das Original, die kleine Fassung ersetzt es danach — hängt das Verkleinern (Display aus), bleibt das Original diff --git a/ROADMAP.md b/ROADMAP.md index b8f736d..4c6c6f5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -62,20 +62,37 @@ Eddy: *„beim Fotografieren kann man den Blitz nicht einstellen und den Fokus a klappt das nicht, wird ohne Einstellung ausgelöst) → 🔦 Licht (Dauerlicht, `track.getCapabilities().torch`, nach dem Auslösen erneut gesetzt). Meldet das Gerät nichts davon, gibt es keinen Knopf. Auswahl bleibt gemerkt (`localStorage baustelle.kamera.blitz`). -- [x] ✅ 19.10.2026: Antippen stellt dort scharf (`pointsOfInterest` + `focusMode: single-shot`, sonst `continuous`; - ohne Punktwahl nur neu scharfstellen), Ring an der Stelle. Bei `single-shot` bleibt der Fokus fest, oben steht - „🎯 Fokus fest · zurück auf Auto" (Tippen → `continuous`). Einmaliger Hinweis „Antippen stellt dort scharf". +- [x] ✅ 19.10.2026: Antippen stellt dort scharf (`pointsOfInterest` + `focusMode`), Ring an der Stelle, einmaliger + Hinweis „Antippen stellt dort scharf". - [x] Lokal geprüft mit nachgebildeter Kamera (Canvas-Strom + gemeldete Fähigkeiten): Reihenfolge der Modi, Licht an/aus, `takePhoto({fillLightMode:"flash"})`, Antippstelle richtig umgerechnet (eingepasste Vorschau, - 0,803 statt 0,75 bei seitlichem Rand), „Fokus fest" → continuous; ohne Fähigkeiten kein Knopf, keine Wirkung. -- [ ] **Am Handy gegenprüfen** (am Schreibtisch nicht nachstellbar): blitzt „An" wirklich, geht „Licht" an, - wird nach Antippen die angetippte Stelle scharf. - Versuch 19.10.2026 am Rechner Buero: Android-Emulator startet nicht (`SVM disabled (by BIOS)`, außerdem fehlen - nach dem Kernel-Update die Module des laufenden Kernels 7.2.9 → erst nach Neustart + SVM im BIOS). Test mit - der Webcam Razer Kiyo Pro in Chrome: Die Kamera hing schon beim Starten des Bildstroms - (`uvcvideo: Failed to set UVC commit control: -110`) und fiel danach vom USB-Bus (`error -71`) — also vor - Blitz/Fokus. Ergebnis daraus nur: Chrome wirft bei nicht unterstützten Fokus-Vorgaben `OverconstrainedError`, - den die App abfängt. + 0,803 statt 0,75 bei seitlichem Rand); ohne Fähigkeiten kein Knopf, keine Wirkung. +- [x] ✅ 19.10.2026 abends: **Gegenprobe im Android-Emulator** (AVD `fluke`, Chrome 153, emulierte Rückkamera mit + Blitz + AF-Bereich) — echter Camera2-Weg, gemessen per `adb shell dumpsys media.camera` („Last request sent"): + + | Einstellung | was bei der Kamera ankommt | Ergebnis | + |---|---|---| + | ⚡ Aus | `aeMode ON`, `flash.mode OFF` | ✓ | + | ⚡ Auto, Auslösen | Standbild `aeMode ON_AUTO_FLASH` | ✓ Kamera entscheidet | + | ⚡ An, Auslösen | Standbild `aeMode ON_ALWAYS_FLASH`, `flash.mode SINGLE` | ✓ wie Kamera-Apps | + | 🔦 Licht | `flash.mode TORCH`, bleibt nach dem Auslösen an | ✓ | + | Antippen (alt) | `afRegions` an falscher Stelle, `afMode` bleibt `CONTINUOUS_PICTURE` | ✗ zwei Fehler | + + **Fehler 1 — Tippstelle ungedreht:** Chrome rechnet `pointsOfInterest` direkt auf die Sensorfläche um + (`VideoCaptureCamera2.java`, keine Drehung), das Sucherbild kommt aber gedreht an (Sensor quer, Bild hochkant). + Tipp bei 67 % / 82 % → Bereich bei 67 % / 82 % des quer liegenden Sensors statt 82 % / 33 %. Jetzt dreht + `sensorPunkt()` zurück (Rückkamera: Sensor 90° − Bildschirmwinkel; Nothing A059 hat 90°, siehe takePhoto- + Drehmarke 6 in KB #1494). Gemessen: hochkant ✓ (zwei Stellen), quer 90° ✓ (1:1), quer 270° ✓ (180°). + **Fehler 2 — „Fokus fest" log:** `single-shot` nimmt Chrome ohne Fehler an (auch mit `exact`), setzt auf Android + aber nichts um — `AF_TRIGGER_START` kommt im Quelltext nicht vor, der Modus bleibt `CONTINUOUS_PICTURE`, und die + gefundene Entfernung meldet Chrome nicht zurück (nur die eigene Vorgabe). Ein echtes Festhalten geht über die + Web-Schnittstelle also nicht. Jetzt: Antippen = Dauer-Autofokus **auf die Tippstelle**, der Hinweis + „🎯 Fokus fest" ist entfernt. +- [x] ✅ 19.10.2026 (Eddy: „den hässlichen Wechselbutton lässt du da doch nicht"): 🔄-Emoji (erschien als blaues + Kästchen) → weißes Material-Symbol wie ✕; Knopf nur, wenn `enumerateDevices()` mindestens zwei Kameras meldet. +- [ ] **Am Handy beim nächsten Einsatz ansehen** — der Weg bis zur Kamera ist belegt, offen ist nur das Gerät selbst: + blitzt das Nothing bei „An" sichtbar, und springt der Fokus nach dem Antippen sofort auf die Stelle (ohne + AF-Auslöser hängt das vom Kamera-Treiber ab). Wenn nicht: Tag + Uhrzeit reichen. ## 7. Galerie-Auswahl: „permission"-Fehler ab dem 3. Foto (gemeldet 2026-09-30) diff --git a/app.css b/app.css index 6b657d6..e89590c 100644 --- a/app.css +++ b/app.css @@ -1312,6 +1312,9 @@ body { .cam-shutter.flash { background: #ffd34d; } /* Blitz-Knopf: Text statt nur Symbol, damit der Zustand lesbar ist (Aus/Auto/An/Licht) */ .camera-modal .cam-blitz { font-size: 15px; white-space: nowrap; } +/* Kamera wechseln: weisses Symbol wie ✕, kein Emoji (das erschien als blaues Kaestchen) */ +.camera-modal .cam-flip { display: inline-flex; align-items: center; } +.camera-modal .cam-flip svg { display: block; } /* Scharfstellen per Antippen */ .camera-modal .cam-fokus-ring { position: absolute; @@ -1326,19 +1329,6 @@ 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; } } -.camera-modal .cam-fokus-fest { - position: absolute; - top: 10px; - left: 50%; - transform: translateX(-50%); - background: rgba(0, 0, 0, 0.6); - color: #ffd34d; - border: 1px solid #ffd34d; - border-radius: 16px; - padding: 6px 12px; - font-size: 13px; - cursor: pointer; -} .camera-modal [hidden] { display: none !important; } /* ============================================================ diff --git a/app.js b/app.js index 75bfa88..e09387e 100644 --- a/app.js +++ b/app.js @@ -3618,13 +3618,14 @@ async function openCameraModal(orderId) {
📷 Kamera
- +
Kamera wird gestartet…
-
@@ -3643,7 +3644,7 @@ async function openCameraModal(orderId) { const stage = modal.querySelector('#cam-stage'); const blitzBtn = modal.querySelector('#cam-blitz'); const fokusRing = modal.querySelector('#cam-fokus-ring'); - const fokusFest = modal.querySelector('#cam-fokus-fest'); + const flipBtn = modal.querySelector('#cam-flip'); let stream = null; let facing = 'environment'; @@ -3687,7 +3688,7 @@ async function openCameraModal(orderId) { modal.querySelector('#cam-close').onclick = () => closeModal(modal); modal.querySelector('#cam-done').onclick = () => closeModal(modal); - modal.querySelector('#cam-flip').onclick = () => { + flipBtn.onclick = () => { facing = (facing === 'environment') ? 'user' : 'environment'; startStream(); }; @@ -3716,6 +3717,11 @@ async function openCameraModal(orderId) { video.srcObject = stream; await video.play().catch(() => {}); hint.style.display = 'none'; + // Wechselknopf nur, wenn es eine zweite Kamera gibt (Emulator, Tablet-Webcam: nur eine). + // Erst nach der Freigabe ist die Geraeteliste vollstaendig. + navigator.mediaDevices.enumerateDevices().then(l => { + if (my === gen) flipBtn.hidden = l.filter(d => d.kind === 'videoinput').length < 2; + }).catch(() => {}); await kameraExtras(my); } catch (err) { if (my !== gen || !document.body.contains(modal)) return; // abgebrochen, nicht mehr relevant @@ -3781,8 +3787,10 @@ async function openCameraModal(orderId) { * - Blitz beim Ausloesen: ImageCapture.getPhotoCapabilities().fillLightMode (auto/flash), * wirkt nur ueber takePhoto({ fillLightMode }) — nicht beim Standbild aus dem Sucher. * - Licht (Dauerlicht): track.getCapabilities().torch, per applyConstraints. - * - Scharfstellen per Antippen: focusMode 'single-shot' (stellt einmal scharf und haelt den - * Fokus fest) bzw. 'continuous', jeweils mit pointsOfInterest = angetippte Stelle (0..1). + * - Scharfstellen per Antippen: pointsOfInterest = angetippte Stelle, focusMode 'continuous' + * → 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). * 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' }; @@ -3791,7 +3799,7 @@ async function openCameraModal(orderId) { let blitzModi = []; let blitz = 'off'; let fotoBlitz = false; // kann takePhoto() blitzen (fillLightMode)? - let fokusModus = ''; // 'single-shot' | 'continuous' | '' (kein Antippen) + let fokusModus = ''; // 'continuous' | 'single-shot' | '' (kein Antippen) let tippHinweis = false; function videoTrack() { @@ -3800,7 +3808,7 @@ async function openCameraModal(orderId) { async function kameraExtras(my) { imageCap = null; blitzModi = []; fotoBlitz = false; fokusModus = ''; - blitzBtn.hidden = true; fokusFest.hidden = true; fokusRing.hidden = true; + blitzBtn.hidden = true; fokusRing.hidden = true; const track = videoTrack(); if (!track) return; let caps = {}; @@ -3832,7 +3840,7 @@ async function openCameraModal(orderId) { } const fm = Array.isArray(caps.focusMode) ? caps.focusMode : []; - fokusModus = fm.includes('single-shot') ? 'single-shot' : (fm.includes('continuous') ? 'continuous' : ''); + fokusModus = fm.includes('continuous') ? 'continuous' : (fm.includes('single-shot') ? 'single-shot' : ''); if (fokusModus && !tippHinweis) { tippHinweis = true; hint.textContent = 'Antippen stellt dort scharf'; @@ -3862,9 +3870,29 @@ async function openCameraModal(orderId) { try { localStorage.setItem(BLITZ_KEY, blitz); } catch (_) {} }; + /* Chrome gibt pointsOfInterest UNGEDREHT an den Sensor weiter (Punkt × Sensorflaeche, + * VideoCaptureCamera2.java) — das Sucherbild kommt aber schon gedreht an: Sensor quer, + * Bild am Handy hochkant. Ohne Rueckdrehung stellte ein Tipp bei 67 % / 82 % auf 67 % / 82 % + * des QUER liegenden Sensors scharf (Emulator, dumpsys afRegions) — also an falscher Stelle. + * Drehung wie in Chrome: Rueckkamera Sensor − Bildschirmwinkel, Frontkamera Sensor + Winkel. + * Sensorlage Rueckkamera 90° (Nothing A059: takePhoto-Drehmarke 6, KB #1494), Frontkamera 270°; + * passt das nicht zum Bildformat (Tablet mit 0°-Sensor), ohne 90°-Anteil. */ + function sensorPunkt(x, y) { + const winkel = (screen.orientation && typeof screen.orientation.angle === 'number') ? screen.orientation.angle : 0; + const drehung = (sensor) => (facing === 'user' ? sensor + winkel : sensor - winkel + 360) % 360; + let d = drehung(facing === 'user' ? 270 : 90); + if ((d % 180 === 90) !== (video.videoHeight > video.videoWidth)) d = drehung(0); + switch (d) { + case 90: return { x: y, y: 1 - x }; + case 180: return { x: 1 - x, y: 1 - y }; + case 270: return { x: 1 - y, y: x }; + default: return { x, y }; + } + } + stage.addEventListener('click', async (ev) => { const track = videoTrack(); - if (!track || !fokusModus || !video.videoWidth || ev.target === fokusFest) return; + if (!track || !fokusModus || !video.videoWidth) return; // 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); @@ -3872,6 +3900,7 @@ async function openCameraModal(orderId) { const x = (ev.clientX - r.left - (r.width - w) / 2) / w; const y = (ev.clientY - r.top - (r.height - h) / 2) / h; if (x < 0 || x > 1 || y < 0 || y > 1) return; + const punkt = sensorPunkt(x, y); const sr = stage.getBoundingClientRect(); fokusRing.style.left = (ev.clientX - sr.left) + 'px'; @@ -3883,7 +3912,7 @@ async function openCameraModal(orderId) { let ok = false; try { - await track.applyConstraints({ advanced: [{ pointsOfInterest: [{ x, y }], focusMode: fokusModus }] }); + await track.applyConstraints({ advanced: [{ pointsOfInterest: [punkt], focusMode: fokusModus }] }); ok = true; } catch (_) { // Ohne Punktwahl wenigstens neu scharfstellen @@ -3891,16 +3920,8 @@ async function openCameraModal(orderId) { } fokusRing.classList.toggle('ok', ok); setTimeout(() => { fokusRing.hidden = true; }, 900); - if (ok && fokusModus === 'single-shot') fokusFest.hidden = false; }); - fokusFest.onclick = async (ev) => { - ev.stopPropagation(); - const track = videoTrack(); - fokusFest.hidden = true; - if (track) { try { await track.applyConstraints({ advanced: [{ focusMode: 'continuous' }] }); } catch (_) {} } - }; - /* Echtes Kamerafoto in voller Sensoraufloesung. * * Ein Standbild aus dem Video-Stream hat hoechstens Stream-Aufloesung (hier bis 4K