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);
+ }
+});