Compare commits

..

No commits in common. "85a64c237aacf3399fa404ab2c251251427bd750" and "0d2f485ecc15239fcefba07a160184fd70489591" have entirely different histories.

5 changed files with 8 additions and 209 deletions

View file

@ -23,8 +23,6 @@ Mobile Progressive Web App für die Baustellen-Doku — Foto-Upload, Sprach- und
**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
- ✅ **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
- ✅ **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
- ✅ **Wake Lock** während Foto-Serie und Upload: schaltet sich das Display ab, friert der Browser laufende Uploads und das Verkleinern ein

View file

@ -47,29 +47,10 @@ Aufträgen: Geteiltes ablegen"*.
klebt am unteren Rand, am Ende lässt sich alles darüber scrollen.
- [ ] Am Handy gegenprüfen: Knopf ist nach dem Update weg (beim ersten Laden räumt die App den leeren Rest ab).
### Befunde aus der AVIF-Prüfung in Bericht (19.10.2026)
- [x] ✅ 19.10.2026: Ist eine Maximalgröße für Fotos gesetzt (`photo_maxside` > 0), behielt das im Browser
verkleinerte JPEG seinen Originalnamen — ein `.avif`/`.webp` lehnte der Server ab. Jetzt zieht
`offline.replaceQueuedBlob()` die Endung auf `.jpg` nach (gilt für Galerie, Teilen und Kamera).
- [x] ✅ 19.10.2026: Beim Verkleinern im Browser (Canvas → JPEG) wurde Transparenz schwarz — jetzt weißer Grund.
Lokal geprüft: Queue-Eintrag „bild.avif" (PNG mit Transparenz) → „bild.jpg", Ecke weiß.
## 9. Kamera: Blitz und Fokus einstellbar (gemeldet 2026-10-19)
Eddy: *„beim Fotografieren kann man den Blitz nicht einstellen und den Fokus auch nicht"*.
- [x] ✅ 19.10.2026: Blitz-Knopf in der Kopfzeile, schaltet reihum durch, was das Gerät meldet: Aus → Auto → An
(`ImageCapture.getPhotoCapabilities().fillLightMode`, wirkt beim Auslösen über `takePhoto({fillLightMode})`;
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] 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.
### Befunde aus der AVIF-Prüfung in Bericht (19.10.2026, nicht geändert)
- [ ] Ist eine Maximalgröße für Fotos gesetzt (`photo_maxside` > 0), behält das im Browser verkleinerte JPEG in
`app.js` `uploadPhoto` seinen Originalnamen — ein `.avif`/`.webp` wird dann vom Server abgelehnt.
- [ ] Beim Verkleinern im Browser (Canvas → JPEG) wird Transparenz schwarz.
## 7. Galerie-Auswahl: „permission"-Fehler ab dem 3. Foto (gemeldet 2026-09-30)

30
app.css
View file

@ -1310,36 +1310,6 @@ body {
}
.cam-shutter:active { transform: scale(0.92); }
.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; }
/* Scharfstellen per Antippen */
.camera-modal .cam-fokus-ring {
position: absolute;
width: 72px;
height: 72px;
margin: -36px 0 0 -36px;
border: 2px solid #fff;
border-radius: 50%;
pointer-events: none;
opacity: 0.9;
}
.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; }
/* ============================================================
Eigene Dialoge (Ersatz für confirm/alert/prompt)

153
app.js
View file

@ -1320,11 +1320,7 @@ async function resizeImage(file, maxSide, timeoutMs, quality) {
const c = document.createElement('canvas');
c.width = Math.round(img.width * scale);
c.height = Math.round(img.height * scale);
const ctx = c.getContext('2d');
// Weisser Grund: JPEG kennt keine Transparenz, ein freigestelltes PNG/WebP wuerde sonst schwarz
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, c.width, c.height);
ctx.drawImage(img, 0, 0, c.width, c.height);
c.getContext('2d').drawImage(img, 0, 0, c.width, c.height);
c.toBlob(b => resolve(b || file), 'image/jpeg', typeof quality === 'number' ? quality : 0.85);
};
img.onerror = () => { URL.revokeObjectURL(url); resolve(file); };
@ -3617,14 +3613,11 @@ async function openCameraModal(orderId) {
<div class="fs-header">
<button class="icon-btn" id="cam-close">✕</button>
<div class="fs-title">📷 Kamera</div>
<button class="icon-btn cam-blitz" id="cam-blitz" title="Blitz" hidden></button>
<button class="icon-btn" id="cam-flip" title="Kamera wechseln">🔄</button>
</div>
<div class="cam-stage" id="cam-stage">
<div class="cam-stage">
<video id="cam-video" playsinline autoplay muted></video>
<div class="cam-hint" id="cam-hint">Kamera wird gestartet…</div>
<div class="cam-fokus-ring" id="cam-fokus-ring" hidden></div>
<button class="cam-fokus-fest" id="cam-fokus-fest" hidden>🎯 Fokus fest · zurück auf Auto</button>
</div>
<div class="cam-filmstrip" id="cam-strip"></div>
<div class="cam-controls">
@ -3640,10 +3633,6 @@ async function openCameraModal(orderId) {
const hint = modal.querySelector('#cam-hint');
const countEl = modal.querySelector('#cam-count');
const shutter = modal.querySelector('#cam-shutter');
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');
let stream = null;
let facing = 'environment';
@ -3716,7 +3705,6 @@ async function openCameraModal(orderId) {
video.srcObject = stream;
await video.play().catch(() => {});
hint.style.display = 'none';
await kameraExtras(my);
} catch (err) {
if (my !== gen || !document.body.contains(modal)) return; // abgebrochen, nicht mehr relevant
console.warn('[Kamera]', err);
@ -3775,132 +3763,6 @@ async function openCameraModal(orderId) {
}
}
/* Blitz und Fokus (Eddy, 19.10.2026: „den Blitz kann man nicht einstellen und den Fokus auch nicht").
*
* Angeboten wird nur, was das Geraet meldet — Chrome kennt es erst, wenn der Strom laeuft:
* - 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).
* 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' };
const BLITZ_KEY = 'baustelle.kamera.blitz';
let imageCap = null;
let blitzModi = [];
let blitz = 'off';
let fotoBlitz = false; // kann takePhoto() blitzen (fillLightMode)?
let fokusModus = ''; // 'single-shot' | 'continuous' | '' (kein Antippen)
let tippHinweis = false;
function videoTrack() {
return stream ? stream.getVideoTracks()[0] || null : null;
}
async function kameraExtras(my) {
imageCap = null; blitzModi = []; fotoBlitz = false; fokusModus = '';
blitzBtn.hidden = true; fokusFest.hidden = true; fokusRing.hidden = true;
const track = videoTrack();
if (!track) return;
let caps = {};
try { caps = typeof track.getCapabilities === 'function' ? (track.getCapabilities() || {}) : {}; } catch (_) {}
let licht = [];
if (typeof ImageCapture === 'function') {
try {
imageCap = new ImageCapture(track);
const pc = await imageCap.getPhotoCapabilities();
licht = Array.isArray(pc && pc.fillLightMode) ? pc.fillLightMode : [];
} catch (_) { /* kein Standbild-Erfasser — Sucherbild bleibt */ }
}
if (my !== gen) return; // inzwischen geschlossen/gewechselt
fotoBlitz = licht.includes('auto') || licht.includes('flash');
const modi = [];
if (fotoBlitz || caps.torch) modi.push('off');
if (licht.includes('auto')) modi.push('auto');
if (licht.includes('flash')) modi.push('flash');
if (caps.torch) modi.push('torch');
blitzModi = modi;
let gemerkt = 'off';
try { gemerkt = localStorage.getItem(BLITZ_KEY) || 'off'; } catch (_) {}
blitz = modi.includes(gemerkt) ? gemerkt : 'off';
if (modi.length > 1) {
blitzBtn.hidden = false;
blitzBtn.textContent = BLITZ_TEXT[blitz];
if (blitz === 'torch') lichtSchalten(true);
}
const fm = Array.isArray(caps.focusMode) ? caps.focusMode : [];
fokusModus = fm.includes('single-shot') ? 'single-shot' : (fm.includes('continuous') ? 'continuous' : '');
if (fokusModus && !tippHinweis) {
tippHinweis = true;
hint.textContent = 'Antippen stellt dort scharf';
hint.style.display = '';
setTimeout(() => { if (hint.textContent === 'Antippen stellt dort scharf') hint.style.display = 'none'; }, 2500);
}
}
async function lichtSchalten(an) {
const track = videoTrack();
if (!track) return false;
try { await track.applyConstraints({ advanced: [{ torch: an }] }); return true; }
catch (_) { return false; }
}
blitzBtn.onclick = async () => {
if (blitzModi.length < 2) return;
const vorher = blitz;
blitz = blitzModi[(blitzModi.indexOf(blitz) + 1) % blitzModi.length];
if (vorher === 'torch' || blitz === 'torch') {
if (!await lichtSchalten(blitz === 'torch') && blitz === 'torch') {
showToast('Das Licht lässt sich hier nicht schalten', 'warn');
blitz = blitzModi[(blitzModi.indexOf(blitz) + 1) % blitzModi.length];
}
}
blitzBtn.textContent = BLITZ_TEXT[blitz];
try { localStorage.setItem(BLITZ_KEY, blitz); } catch (_) {}
};
stage.addEventListener('click', async (ev) => {
const track = videoTrack();
if (!track || !fokusModus || !video.videoWidth || ev.target === fokusFest) 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);
const w = video.videoWidth * sk, h = video.videoHeight * sk;
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 sr = stage.getBoundingClientRect();
fokusRing.style.left = (ev.clientX - sr.left) + 'px';
fokusRing.style.top = (ev.clientY - sr.top) + 'px';
fokusRing.hidden = false;
fokusRing.classList.remove('ok', 'aktiv');
void fokusRing.offsetWidth; // Animation neu starten
fokusRing.classList.add('aktiv');
let ok = false;
try {
await track.applyConstraints({ advanced: [{ pointsOfInterest: [{ x, y }], focusMode: fokusModus }] });
ok = true;
} catch (_) {
// Ohne Punktwahl wenigstens neu scharfstellen
try { await track.applyConstraints({ advanced: [{ focusMode: fokusModus }] }); ok = true; } catch (_) {}
}
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
@ -3915,15 +3777,8 @@ async function openCameraModal(orderId) {
if (typeof ImageCapture === 'undefined' || !stream) return null;
const track = stream.getVideoTracks()[0];
if (!track || typeof track.getCapabilities !== 'function') return null;
const cap = imageCap || new ImageCapture(track);
let blob = null;
if (fotoBlitz && blitz !== 'torch') {
// Blitz ausdruecklich setzen (auch „aus"); klappt das nicht, ohne Einstellungen ausloesen
try { blob = await cap.takePhoto({ fillLightMode: blitz }); } catch (_) { blob = null; }
}
if (!blob) blob = await cap.takePhoto();
// Manche Geraete schalten das Dauerlicht beim Ausloesen ab
if (blitz === 'torch') lichtSchalten(true);
const cap = new ImageCapture(track);
const blob = await cap.takePhoto();
return (blob && blob.size > 0) ? blob : null;
} catch (_) {
return null; // z.B. „takePhoto not supported" — Standbild nehmen

View file

@ -74,11 +74,6 @@
const it = await idb.queuePatch(id, (x) => {
x.data = buf;
x.mime = fileBlob.type || x.mime || 'image/jpeg';
// Verkleinert = JPEG aus dem Canvas: Endung angleichen. Sonst laege JPEG-Inhalt unter
// „.avif"/„.webp" in der Queue, und der Server lehnt die Endung ab.
if (x.mime === 'image/jpeg' && x.filename && !/\.jpe?g$/i.test(x.filename)) {
x.filename = x.filename.replace(/\.[^.\/]*$/, '') + '.jpg';
}
});
return !!it; // null: schon hochgeladen und geloescht
}