From 91f3f483b56cf7ce5e0ce68c8486a4ecc7d82b99 Mon Sep 17 00:00:00 2001 From: Eddy Date: Mon, 19 Oct 2026 22:09:35 +0200 Subject: [PATCH] =?UTF-8?q?Verkleinerte=20Fotos=20hei=C3=9Fen=20.jpg,=20Tr?= =?UTF-8?q?ansparenz=20wird=20wei=C3=9F=20statt=20schwarz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ist eine Maximalgröße eingestellt, ersetzte das im Browser verkleinerte JPEG den Inhalt in der Queue, der Name blieb aber z. B. „.avif"/„.webp" — der Server lehnte das ab. replaceQueuedBlob() zieht die Endung jetzt auf .jpg nach (Galerie, Teilen, Kamera). Beim Verkleinern wird vor dem Zeichnen weiß gefüllt, sonst wurden transparente Flächen im JPEG schwarz. Co-Authored-By: Claude Opus 5.5 --- app.js | 153 +++++++++++++++++++++++++++++++++++++++++++++++-- lib/offline.js | 5 ++ 2 files changed, 154 insertions(+), 4 deletions(-) diff --git a/app.js b/app.js index 870a244..75bfa88 100644 --- a/app.js +++ b/app.js @@ -1320,7 +1320,11 @@ 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); - c.getContext('2d').drawImage(img, 0, 0, c.width, c.height); + 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.toBlob(b => resolve(b || file), 'image/jpeg', typeof quality === 'number' ? quality : 0.85); }; img.onerror = () => { URL.revokeObjectURL(url); resolve(file); }; @@ -3613,11 +3617,14 @@ async function openCameraModal(orderId) {
📷 Kamera
+
-
+
Kamera wird gestartet…
+ +
@@ -3633,6 +3640,10 @@ 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'; @@ -3705,6 +3716,7 @@ 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); @@ -3763,6 +3775,132 @@ 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 @@ -3777,8 +3915,15 @@ 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 = new ImageCapture(track); - const blob = await cap.takePhoto(); + 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); return (blob && blob.size > 0) ? blob : null; } catch (_) { return null; // z.B. „takePhoto not supported" — Standbild nehmen diff --git a/lib/offline.js b/lib/offline.js index c578113..366a79d 100644 --- a/lib/offline.js +++ b/lib/offline.js @@ -74,6 +74,11 @@ 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 }