diff --git a/app.css b/app.css
index 0f8f5be..4a5dfb9 100644
--- a/app.css
+++ b/app.css
@@ -715,3 +715,79 @@ body {
/* help-btn in Topbar kleiner machen */
#help-btn { font-size: 18px; padding: 4px 8px; }
+
+/* ========== Photo Viewer mit Zoom + Swipe ========== */
+.photo-viewer-modal .pv-body {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ touch-action: none;
+ background: #000;
+}
+.photo-viewer-modal #pv-image {
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: contain;
+ transform-origin: center center;
+ transition: transform 0.15s ease-out, opacity 0.2s;
+ cursor: grab;
+ user-select: none;
+ -webkit-user-drag: none;
+}
+.photo-viewer-modal #pv-image.dragging {
+ cursor: grabbing;
+ transition: none;
+}
+.photo-viewer-modal .pv-counter {
+ font-size: 13px;
+ color: #888;
+ margin-left: auto;
+ margin-right: 8px;
+}
+.photo-viewer-modal .pv-nav {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 50px;
+ height: 80px;
+ border: none;
+ background: rgba(255, 255, 255, 0.1);
+ color: #fff;
+ font-size: 32px;
+ cursor: pointer;
+ z-index: 10;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 4px;
+}
+.photo-viewer-modal .pv-nav:disabled { opacity: 0.2; cursor: default; }
+.photo-viewer-modal .pv-prev { left: 8px; }
+.photo-viewer-modal .pv-next { right: 8px; }
+.photo-viewer-modal .pv-zoom-btns {
+ position: absolute;
+ bottom: 16px;
+ right: 12px;
+ display: flex;
+ gap: 8px;
+ z-index: 10;
+}
+.photo-viewer-modal .pv-zoom-btns button {
+ width: 40px;
+ height: 40px;
+ border: none;
+ background: rgba(255, 255, 255, 0.15);
+ color: #fff;
+ font-size: 20px;
+ cursor: pointer;
+ border-radius: 6px;
+}
+.photo-viewer-modal .pv-zoom-btns button:active {
+ background: rgba(255, 255, 255, 0.3);
+}
+@media (max-width: 600px) {
+ .photo-viewer-modal .pv-nav { width: 40px; height: 60px; font-size: 24px; }
+ .photo-viewer-modal .pv-zoom-btns button { width: 36px; height: 36px; font-size: 18px; }
+}
diff --git a/app.js b/app.js
index 7bd8b55..17265ad 100644
--- a/app.js
+++ b/app.js
@@ -1514,41 +1514,185 @@ function escapeHtml(s) {
}
/* ============================================================
- * PHOTO VOLLBILD MODAL + SKIZZEN-EDITOR
+ * PHOTO VOLLBILD MODAL MIT ZOOM + SWIPE + SKIZZEN-EDITOR
* ============================================================ */
async function openPhotoModal(orderId, relpath) {
- const url = await api.getPhotoBlobUrl(relpath);
- if (!url) { showToast('Foto konnte nicht geladen werden', 'error'); return; }
+ // Alle Bilder im Grid sammeln für Navigation
+ const allThumbs = Array.from(document.querySelectorAll('#photo-grid .thumb[data-relpath]'));
+ const allRelpaths = allThumbs.map(t => t.dataset.relpath);
+ let currentIndex = allRelpaths.indexOf(relpath);
+ if (currentIndex < 0) currentIndex = 0;
+
+ // Zoom/Pan State
+ let zoom = 1, panX = 0, panY = 0;
+ let currentUrl = null;
+ const touchState = { startX: 0, startY: 0, startDist: 0, startZoom: 1, startPanX: 0, startPanY: 0, isDragging: false, isPinching: false, lastTap: 0 };
const modal = document.createElement('div');
- modal.className = 'fullscreen-modal';
+ modal.className = 'fullscreen-modal photo-viewer-modal';
modal.innerHTML = `
- 
+
+
![]()
+
+
+
+
+
+
+
+
`;
document.body.appendChild(modal);
- const close = () => modal.remove();
+ const img = modal.querySelector('#pv-image');
+ const body = modal.querySelector('#pv-body');
+ const titleEl = modal.querySelector('#pv-title');
+ const counter = modal.querySelector('#pv-counter');
+ const prevBtn = modal.querySelector('#pv-prev');
+ const nextBtn = modal.querySelector('#pv-next');
+
+ function resetZoom() { zoom = 1; panX = 0; panY = 0; applyTransform(); }
+ function applyTransform() { img.style.transform = `translate(${panX}px, ${panY}px) scale(${zoom})`; }
+ function setZoom(z) {
+ zoom = Math.max(0.5, Math.min(5, z));
+ constrainPan();
+ applyTransform();
+ }
+ function constrainPan() {
+ if (zoom <= 1) { panX = 0; panY = 0; return; }
+ const rect = body.getBoundingClientRect();
+ const maxX = Math.max(0, (img.naturalWidth * zoom - rect.width) / 2);
+ const maxY = Math.max(0, (img.naturalHeight * zoom - rect.height) / 2);
+ panX = Math.max(-maxX, Math.min(maxX, panX));
+ panY = Math.max(-maxY, Math.min(maxY, panY));
+ }
+
+ async function loadImage(idx) {
+ if (idx < 0 || idx >= allRelpaths.length) return;
+ currentIndex = idx;
+ const rp = allRelpaths[idx];
+ titleEl.textContent = rp.split('/').pop();
+ counter.textContent = `${idx + 1} / ${allRelpaths.length}`;
+ prevBtn.disabled = idx === 0;
+ nextBtn.disabled = idx === allRelpaths.length - 1;
+ prevBtn.style.display = nextBtn.style.display = allRelpaths.length > 1 ? '' : 'none';
+
+ img.style.opacity = '0.3';
+ const url = await api.getPhotoBlobUrl(rp);
+ if (url) {
+ currentUrl = url;
+ img.src = url;
+ img.onload = () => { img.style.opacity = '1'; };
+ }
+ resetZoom();
+ }
+
+ function close() { modal.remove(); document.removeEventListener('keydown', keyHandler); }
+ function goPrev() { if (currentIndex > 0) loadImage(currentIndex - 1); }
+ function goNext() { if (currentIndex < allRelpaths.length - 1) loadImage(currentIndex + 1); }
+
+ // Events
modal.querySelector('#fs-close').onclick = close;
+ prevBtn.onclick = (e) => { e.stopPropagation(); goPrev(); };
+ nextBtn.onclick = (e) => { e.stopPropagation(); goNext(); };
+ modal.querySelector('#pv-zin').onclick = () => setZoom(zoom + 0.5);
+ modal.querySelector('#pv-zout').onclick = () => setZoom(zoom - 0.5);
+ modal.querySelector('#pv-zreset').onclick = resetZoom;
+
modal.querySelector('#fs-delete').onclick = async () => {
if (!confirm('Foto wirklich löschen?')) return;
try {
- await api.deletePhoto(relpath);
+ await api.deletePhoto(allRelpaths[currentIndex]);
showToast('✓ Gelöscht');
close();
- // Parent view neu laden
router.navigate();
} catch (e) { showToast('Löschen fehlgeschlagen: ' + e.message, 'error'); }
};
modal.querySelector('#fs-sketch').onclick = () => {
close();
- openSketchEditor(orderId, url, relpath);
+ openSketchEditor(orderId, currentUrl, allRelpaths[currentIndex]);
};
+
+ // Doppelklick = Zoom Toggle
+ img.addEventListener('dblclick', (e) => {
+ e.preventDefault();
+ if (zoom > 1) resetZoom(); else setZoom(2.5);
+ });
+
+ // Touch: Pinch-to-Zoom + Swipe + Pan
+ body.addEventListener('touchstart', (e) => {
+ if (e.touches.length === 1) {
+ const t = e.touches[0];
+ touchState.startX = t.clientX;
+ touchState.startY = t.clientY;
+ touchState.startPanX = panX;
+ touchState.startPanY = panY;
+ touchState.isDragging = true;
+ touchState.isPinching = false;
+ const now = Date.now();
+ if (now - touchState.lastTap < 300) {
+ if (zoom > 1) resetZoom(); else setZoom(2.5);
+ touchState.lastTap = 0;
+ } else {
+ touchState.lastTap = now;
+ }
+ } else if (e.touches.length === 2) {
+ e.preventDefault();
+ touchState.isPinching = true;
+ touchState.isDragging = false;
+ touchState.startDist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
+ touchState.startZoom = zoom;
+ }
+ }, { passive: false });
+
+ body.addEventListener('touchmove', (e) => {
+ if (touchState.isPinching && e.touches.length === 2) {
+ e.preventDefault();
+ const dist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
+ setZoom(touchState.startZoom * (dist / touchState.startDist));
+ } else if (touchState.isDragging && e.touches.length === 1) {
+ const t = e.touches[0];
+ const dx = t.clientX - touchState.startX;
+ const dy = t.clientY - touchState.startY;
+ if (zoom > 1) {
+ e.preventDefault();
+ panX = touchState.startPanX + dx;
+ panY = touchState.startPanY + dy;
+ constrainPan();
+ applyTransform();
+ }
+ }
+ }, { passive: false });
+
+ body.addEventListener('touchend', (e) => {
+ if (touchState.isDragging && zoom <= 1 && e.changedTouches.length > 0) {
+ const t = e.changedTouches[0];
+ const dx = t.clientX - touchState.startX;
+ if (Math.abs(dx) > 50) {
+ if (dx > 0) goPrev(); else goNext();
+ }
+ }
+ touchState.isDragging = false;
+ touchState.isPinching = false;
+ });
+
+ // Tastatur
+ const keyHandler = (e) => {
+ if (e.key === 'Escape') close();
+ if (e.key === 'ArrowLeft') goPrev();
+ if (e.key === 'ArrowRight') goNext();
+ };
+ document.addEventListener('keydown', keyHandler);
+
+ // Erstes Bild laden
+ await loadImage(currentIndex);
}
async function openVoiceModal(orderId) {