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 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
}