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 = `
-
${escapeHtml(relpath.split('/').pop())}
+
+
-
+
+ +
+ + +
+ + + +
`; 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) {