Compare commits
No commits in common. "85a64c237aacf3399fa404ab2c251251427bd750" and "0d2f485ecc15239fcefba07a160184fd70489591" have entirely different histories.
85a64c237a
...
0d2f485ecc
5 changed files with 8 additions and 209 deletions
|
|
@ -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
|
||||
|
|
|
|||
27
ROADMAP.md
27
ROADMAP.md
|
|
@ -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
30
app.css
|
|
@ -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
153
app.js
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue