diff --git a/mobile_upload.php b/mobile_upload.php index bba1554..411ec93 100644 --- a/mobile_upload.php +++ b/mobile_upload.php @@ -514,6 +514,123 @@ body { color: #fff; font-size: 14px; } + +/* ========== Bild-Viewer (Zoom + Swipe) ========== */ +.pwa-viewer-overlay { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0, 0, 0, 0.95); + z-index: 99999; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + visibility: hidden; + transition: opacity 0.2s ease, visibility 0.2s ease; + touch-action: none; +} +.pwa-viewer-overlay.active { + opacity: 1; + visibility: visible; +} +.pwa-viewer-container { + position: relative; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} +.pwa-viewer-image { + max-width: 100vw; + max-height: 100vh; + object-fit: contain; + transform-origin: center center; + transition: transform 0.15s ease-out; + cursor: grab; +} +.pwa-viewer-image.dragging { + cursor: grabbing; + transition: none; +} +.pwa-viewer-close { + position: absolute; + top: 12px; + right: 12px; + width: 44px; + height: 44px; + border: none; + background: rgba(255, 255, 255, 0.2); + color: #fff; + font-size: 28px; + line-height: 1; + cursor: pointer; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} +.pwa-viewer-nav { + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 50px; + height: 70px; + border: none; + background: rgba(255, 255, 255, 0.15); + color: #fff; + font-size: 28px; + cursor: pointer; + z-index: 10; +} +.pwa-viewer-nav:disabled { opacity: 0.3; } +.pwa-viewer-nav.prev { left: 8px; border-radius: 4px; } +.pwa-viewer-nav.next { right: 8px; border-radius: 4px; } +.pwa-viewer-counter { + position: absolute; + bottom: 16px; + left: 50%; + transform: translateX(-50%); + color: #fff; + font-size: 14px; + background: rgba(0, 0, 0, 0.6); + padding: 6px 14px; + border-radius: 20px; + z-index: 10; +} +.pwa-viewer-zoom { + position: absolute; + bottom: 16px; + right: 12px; + display: flex; + gap: 8px; + z-index: 10; +} +.pwa-viewer-zoom button { + width: 40px; + height: 40px; + border: none; + background: rgba(255, 255, 255, 0.2); + color: #fff; + font-size: 20px; + cursor: pointer; + border-radius: 6px; +} + +/* Hochgeladene Bilder klickbar */ +.uploaded-item { + cursor: pointer; +} +.uploaded-item:active { + background: rgba(255,255,255,0.1); +} + +/* Scanner-Thumbnails klickbar */ +.scanner-page-thumb { + cursor: pointer; +} @@ -1075,6 +1192,206 @@ function toast(msg, isError) { function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } + +// === BILD-VIEWER mit Zoom und Swipe === +const pwaViewer = (function() { + let images = []; + let currentIndex = 0; + let zoom = 1; + let panX = 0, panY = 0; + let overlay, container, img, closeBtn, prevBtn, nextBtn, counter; + let touchState = { startX: 0, startY: 0, startDist: 0, startZoom: 1, startPanX: 0, startPanY: 0, isDragging: false, isPinching: false, lastTap: 0 }; + + function init() { + // DOM erstellen + overlay = document.createElement('div'); + overlay.className = 'pwa-viewer-overlay'; + overlay.innerHTML = ` +
+ +
+ + + +
1 / 1
+
+ + + +
+ `; + document.body.appendChild(overlay); + + container = overlay.querySelector('.pwa-viewer-container'); + img = overlay.querySelector('.pwa-viewer-image'); + closeBtn = overlay.querySelector('.pwa-viewer-close'); + prevBtn = overlay.querySelector('.pwa-viewer-nav.prev'); + nextBtn = overlay.querySelector('.pwa-viewer-nav.next'); + counter = overlay.querySelector('.pwa-viewer-counter'); + + // Events + closeBtn.onclick = close; + overlay.onclick = (e) => { if (e.target === overlay || e.target === container) close(); }; + prevBtn.onclick = (e) => { e.stopPropagation(); prev(); }; + nextBtn.onclick = (e) => { e.stopPropagation(); next(); }; + + overlay.querySelector('.pwa-viewer-zoom').onclick = (e) => { + e.stopPropagation(); + const a = e.target.dataset.action; + if (a === 'in') setZoom(zoom + 0.5); + else if (a === 'out') setZoom(zoom - 0.5); + else if (a === 'reset') resetZoom(); + }; + + // Touch + container.addEventListener('touchstart', handleTouchStart, { passive: false }); + container.addEventListener('touchmove', handleTouchMove, { passive: false }); + container.addEventListener('touchend', handleTouchEnd); + + // Doppelklick + img.addEventListener('dblclick', (e) => { + e.preventDefault(); + if (zoom > 1) resetZoom(); + else setZoom(2.5); + }); + } + + function open(imgs, idx) { + images = imgs; + currentIndex = Math.max(0, Math.min(idx || 0, imgs.length - 1)); + resetZoom(); + loadCurrent(); + overlay.classList.add('active'); + document.body.style.overflow = 'hidden'; + } + + function close() { + overlay.classList.remove('active'); + document.body.style.overflow = ''; + } + + function next() { if (currentIndex < images.length - 1) { currentIndex++; resetZoom(); loadCurrent(); } } + function prev() { if (currentIndex > 0) { currentIndex--; resetZoom(); loadCurrent(); } } + + function loadCurrent() { + img.src = images[currentIndex].url; + counter.textContent = (currentIndex + 1) + ' / ' + images.length; + prevBtn.disabled = currentIndex === 0; + nextBtn.disabled = currentIndex === images.length - 1; + prevBtn.style.display = nextBtn.style.display = images.length > 1 ? '' : 'none'; + } + + function setZoom(z) { + zoom = Math.max(0.5, Math.min(5, z)); + constrainPan(); + applyTransform(); + } + + function resetZoom() { zoom = 1; panX = 0; panY = 0; applyTransform(); } + + function constrainPan() { + if (zoom <= 1) { panX = 0; panY = 0; return; } + const rect = container.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)); + } + + function applyTransform() { + img.style.transform = `translate(${panX}px, ${panY}px) scale(${zoom})`; + } + + function handleTouchStart(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; + // Doppeltap + 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; + } + } + + function handleTouchMove(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(); + } + } + } + + function handleTouchEnd(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) prev(); else next(); + } + } + touchState.isDragging = false; + touchState.isPinching = false; + } + + init(); + return { open, close }; +})(); + +// Hochgeladene Bilder klickbar machen +uploadedList.addEventListener('click', (e) => { + const item = e.target.closest('.uploaded-item'); + if (!item) return; + // Alle Bilder in der Liste sammeln + const items = Array.from(uploadedList.querySelectorAll('.uploaded-item')); + const images = items.map(it => { + const name = it.querySelector('.name')?.textContent || ''; + // Bild-URL aus dem Upload-Ordner (über Token-API holen wäre besser, aber als Fallback filename) + return { url: '', title: name }; + }).filter(i => i.title); + + // Da wir die URL nicht haben, zeigen wir nur eine Meldung + // Die Bilder sind erst nach Upload auf dem Server, nicht mehr lokal verfügbar + toast('Bilder nur im Dolibarr-Editor ansehbar'); +}); + +// Scanner-Thumbs klickbar machen (diese haben noch die lokale URL) +scannerPagesStrip.addEventListener('click', (e) => { + const thumb = e.target.closest('.scanner-page-thumb'); + if (!thumb) return; + + // Alle Scanner-Seiten als Bilder + const images = scannedPages.map((p, i) => ({ url: p.url, title: 'Seite ' + (i + 1) })); + const idx = Array.from(scannerPagesStrip.querySelectorAll('.scanner-page-thumb')).indexOf(thumb); + if (images.length > 0) { + pwaViewer.open(images, idx >= 0 ? idx : 0); + } +});