let isPinching = false; let pinchZoomOccurred = false; let initialPinchDistance = null; let initialScale = 1; let currentScale = 1; let currentTranslateX = 0; let currentTranslateY = 0; let isPanning = false; let panStartX = 0; let panStartY = 0; let panOriginX = 0; let panOriginY = 0; let pinchStartMidX = 0; let pinchStartMidY = 0; let pinchStartTranslateX = 0; let pinchStartTranslateY = 0; let exifAbortController = null; let isGalleryLoading = false; let autoPlayTimer = null; let isAutoPlay = false; let navHideTimer = null; const AUTO_PLAY_INTERVAL_MS = 5000; let pendingAutoAdvance = false; // Assume that currentGalleryImages is already declared in app.js. // Only declare currentGalleryIndex if it isn't defined already. if (typeof currentGalleryIndex === "undefined") { var currentGalleryIndex = -1; } let currentExifRequestId = 0; const previewRefreshInFlight = new Set(); function encodeGalleryPath(relUrl) { if (!relUrl) { return ''; } return relUrl .split('/') .map(segment => encodeURIComponent(segment)) .join('/'); } function findBrowserThumbnailByRelUrl(relUrl) { if (!relUrl) { return null; } const links = document.querySelectorAll('.image-link[data-url]'); for (const link of links) { if ((link.dataset.url || '') === relUrl) { return link.querySelector('img.thumbnail'); } } return null; } function isFallbackThumbnail(imgEl) { if (!imgEl) { return false; } const src = imgEl.getAttribute('src') || ''; return imgEl.classList.contains('thumbnail-fallback') || src.startsWith('data:image/'); } function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function refreshBrowserThumbnailIfMissing(relUrl) { if (!relUrl || previewRefreshInFlight.has(relUrl)) { return; } const thumbImg = findBrowserThumbnailByRelUrl(relUrl); if (!thumbImg || !isFallbackThumbnail(thumbImg)) { return; } const baseThumbUrl = thumbImg.dataset.thumbUrl; if (!baseThumbUrl) { return; } previewRefreshInFlight.add(relUrl); const separator = baseThumbUrl.includes('?') ? '&' : '?'; try { for (let attempt = 0; attempt < 2; attempt++) { const refreshUrl = `${baseThumbUrl}${separator}_=${Date.now()}-${attempt}`; const response = await fetch(refreshUrl, { method: 'GET', cache: 'no-store' }).catch(() => null); if (response && response.ok && response.status !== 204) { thumbImg.classList.remove('thumbnail-fallback'); if (typeof handleThumbnailLoad === 'function') { thumbImg.onload = () => handleThumbnailLoad(thumbImg); } if (typeof handleThumbnailError === 'function') { thumbImg.onerror = () => handleThumbnailError(thumbImg); } thumbImg.src = refreshUrl; return; } if (attempt === 0) { await delay(250); } } } finally { previewRefreshInFlight.delete(relUrl); } } function escapeHtml(value) { return String(value === undefined || value === null ? '' : value).replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char])); } function renderExifData(exif) { const exifPanel = document.getElementById('gallery-exif'); if (!exifPanel) { return; } const entries = exif && Object.keys(exif).length > 0 ? exif : null; if (!entries) { exifPanel.innerHTML = '
No EXIF data
'; return; } const importantKeys = [ 'Model', 'Make', 'LensModel', 'DateTimeOriginal', 'ExposureTime', 'FNumber', 'ISOSpeedRatings', 'FocalLength', 'FocalLengthIn35mmFilm', 'ExposureBiasValue', 'WhiteBalance', 'Software' ]; const rows = []; importantKeys.forEach(key => { const value = exif[key]; if (value !== undefined && value !== null && value !== '') { rows.push([key, value]); } }); if (rows.length === 0) { exifPanel.innerHTML = '
No EXIF data
'; return; } exifPanel.innerHTML = rows.map(([key, value]) => { const safeKey = escapeHtml(key); const safeValue = escapeHtml(Array.isArray(value) ? value.join(', ') : value); return `
${safeKey}${safeValue}
`; }).join(''); } function showExifPlaceholder(message) { const exifPanel = document.getElementById('gallery-exif'); if (!exifPanel) { return; } exifPanel.innerHTML = `
${message}
`; } function loadExifData(relUrl) { const exifPanel = document.getElementById('gallery-exif'); const infoButton = document.getElementById('gallery-info'); if (!exifPanel || !infoButton) { return; } const requestId = ++currentExifRequestId; if (exifAbortController) { try { exifAbortController.abort(); } catch (e) { /* ignore */ } } exifAbortController = typeof AbortController !== 'undefined' ? new AbortController() : null; infoButton.classList.add('loading'); showExifPlaceholder('Loading EXIF...'); fetch(`/media_exif/${encodeGalleryPath(relUrl)}`, exifAbortController ? { signal: exifAbortController.signal } : {}) .then(res => { if (!res.ok) { throw new Error('EXIF fetch failed'); } return res.json(); }) .then(data => { if (requestId !== currentExifRequestId) { return; } infoButton.classList.remove('loading'); renderExifData((data && data.exif) || {}); }) .catch((err) => { if (err && err.name === 'AbortError') { return; } if (requestId !== currentExifRequestId) { return; } infoButton.classList.remove('loading'); showExifPlaceholder('EXIF not available'); }); } function toggleExifPanel() { const exifPanel = document.getElementById('gallery-exif'); const infoButton = document.getElementById('gallery-info'); if (!exifPanel || !infoButton) { return; } const isOpen = exifPanel.classList.toggle('open'); infoButton.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); } function resetExifPanel(clearOpen = true) { const exifPanel = document.getElementById('gallery-exif'); const infoButton = document.getElementById('gallery-info'); currentExifRequestId++; if (exifAbortController) { try { exifAbortController.abort(); } catch (e) { /* ignore */ } exifAbortController = null; } if (exifPanel) { exifPanel.innerHTML = ''; if (clearOpen) { exifPanel.classList.remove('open'); } } if (infoButton && clearOpen) { infoButton.setAttribute('aria-expanded', 'false'); infoButton.classList.remove('loading'); } } function updateFilenameDisplay(relUrl) { const filenameEl = document.getElementById('gallery-filename'); if (!filenameEl) { return; } const filenameTextEl = filenameEl.querySelector('.filename-text'); if (!relUrl) { if (filenameTextEl) { filenameTextEl.textContent = ''; } else { filenameEl.textContent = ''; } filenameEl.dataset.filename = ''; return; } const parts = relUrl.split('/'); const filename = parts[parts.length - 1] || relUrl; if (filenameTextEl) { filenameTextEl.textContent = filename; } else { filenameEl.textContent = filename; } filenameEl.dataset.filename = filename; } function updateFilenameProgress() { const filenameEl = document.getElementById('gallery-filename'); if (!filenameEl) { return; } const total = currentGalleryImages.length; const hasValidIndex = currentGalleryIndex >= 0 && currentGalleryIndex < total; const progress = total > 0 && hasValidIndex ? ((currentGalleryIndex + 1) / total) * 100 : 0; filenameEl.style.setProperty('--filename-progress', `${progress}%`); } function updateGalleryPosition() { const positionEl = document.getElementById('gallery-position'); if (!positionEl) { return; } const total = currentGalleryImages.length; if (total <= 0 || currentGalleryIndex < 0 || currentGalleryIndex >= total) { positionEl.textContent = ''; return; } positionEl.textContent = `${currentGalleryIndex + 1} / ${total}`; } function updateDownloadLink(relUrl) { const downloadBtn = document.getElementById('gallery-download'); if (!downloadBtn) { return; } if (!relUrl) { downloadBtn.removeAttribute('data-relurl'); return; } // Store relUrl for the click handler (which fetches a short-lived token) downloadBtn.dataset.relurl = relUrl; } function updateNextButtonState() { const nextButton = document.querySelector('.gallery-next'); if (!nextButton) { return; } const atLastImage = currentGalleryIndex >= currentGalleryImages.length - 1; nextButton.textContent = atLastImage ? '\u00D7' : '\u203A'; nextButton.setAttribute('aria-label', atLastImage ? 'Close gallery' : 'Next image'); nextButton.classList.toggle('is-close', atLastImage); } function showGalleryNav() { clearTimeout(navHideTimer); document.querySelectorAll('.gallery-nav').forEach(btn => btn.classList.remove('nav-hidden')); if (currentScale > 1.01) { return; } navHideTimer = setTimeout(() => { document.querySelectorAll('.gallery-nav').forEach(btn => btn.classList.add('nav-hidden')); }, 1000); } function hideGalleryNav() { clearTimeout(navHideTimer); document.querySelectorAll('.gallery-nav').forEach(btn => btn.classList.add('nav-hidden')); } function clamp(value, min, max) { return Math.min(Math.max(value, min), max); } function getPanBounds(image, scale) { const stage = document.getElementById('gallery-stage'); if (!stage || !image) { return { maxX: 0, maxY: 0 }; } const baseWidth = image.clientWidth || 0; const baseHeight = image.clientHeight || 0; const scaledWidth = baseWidth * scale; const scaledHeight = baseHeight * scale; return { maxX: Math.max(0, (scaledWidth - stage.clientWidth) / 2), maxY: Math.max(0, (scaledHeight - stage.clientHeight) / 2) }; } function applyImageTransform(image) { if (!image) { return; } const { maxX, maxY } = getPanBounds(image, currentScale); currentTranslateX = clamp(currentTranslateX, -maxX, maxX); currentTranslateY = clamp(currentTranslateY, -maxY, maxY); image.style.transform = `translate(-50%, -50%) translate(${currentTranslateX}px, ${currentTranslateY}px) scale(${currentScale})`; } function resetImageTransformState(image = null) { isPinching = false; isPanning = false; pinchZoomOccurred = false; initialPinchDistance = null; initialScale = 1; currentScale = 1; currentTranslateX = 0; currentTranslateY = 0; panStartX = 0; panStartY = 0; panOriginX = 0; panOriginY = 0; pinchStartMidX = 0; pinchStartMidY = 0; pinchStartTranslateX = 0; pinchStartTranslateY = 0; if (image) { applyImageTransform(image); } } function openGalleryModal(relUrl) { const modal = document.getElementById('gallery-modal'); if (!modal) { return; } document.body.style.overflow = 'hidden'; // Disable background scrolling. currentGalleryIndex = currentGalleryImages.indexOf(relUrl); if (currentGalleryIndex < 0) { currentGalleryIndex = 0; } modal.setAttribute('aria-hidden', 'false'); showGalleryImage(relUrl); modal.style.display = 'flex'; showGalleryNav(); } function showGalleryImage(relUrl) { if (isGalleryLoading) { return; } isGalleryLoading = true; clearAutoPlayTimer(); updateNextButtonState(); updateGalleryPosition(); updateFilenameProgress(); loadExifData(relUrl); const fullUrl = '/media/' + relUrl; const stage = document.getElementById('gallery-stage'); const loader = document.getElementById('loader-container'); if (!stage || !loader) { isGalleryLoading = false; return; } updateDownloadLink(relUrl); // Set a timeout for showing the loader after 500ms. let loaderTimeout = setTimeout(() => { loader.style.display = 'flex'; }, 500); // Preload the new image. const tempImg = new Image(); tempImg.src = fullUrl; tempImg.onload = function () { clearTimeout(loaderTimeout); loader.style.display = 'none'; updateFilenameDisplay(relUrl); refreshBrowserThumbnailIfMissing(relUrl); // Select all current images in the gallery stage const existingImages = stage.querySelectorAll('img'); // Create a new image element for the transition const newImage = document.createElement('img'); newImage.src = fullUrl; newImage.style.position = 'absolute'; newImage.style.top = '50%'; // Center vertically newImage.style.left = '50%'; // Center horizontally newImage.style.transform = 'translate(-50%, -50%)'; // Ensure centering newImage.style.transition = 'opacity 0.5s ease-in-out'; newImage.style.opacity = 0; // Start fully transparent newImage.id = 'gallery-modal-content'; newImage.alt = ''; // Add pinch-to-zoom event listeners newImage.addEventListener('touchstart', handlePinchStart, { passive: false }); newImage.addEventListener('touchmove', handlePinchMove, { passive: false }); newImage.addEventListener('touchend', handlePinchEnd, { passive: false }); newImage.addEventListener('touchcancel', handlePinchEnd, { passive: false }); // Reset pinch variables for the new image resetImageTransformState(newImage); // Append new image on top of existing ones stage.appendChild(newImage); // Fade out all old images and fade in the new one setTimeout(() => { existingImages.forEach(img => { img.style.opacity = 0; // Apply fade-out }); newImage.style.opacity = 1; // Fade in the new image }, 10); // Wait for fade-out to finish, then remove all old images setTimeout(() => { existingImages.forEach(img => { if (img.parentNode) { img.parentNode.removeChild(img); // Remove old images } }); isGalleryLoading = false; if (pendingAutoAdvance && isAutoPlay) { pendingAutoAdvance = false; attemptAutoAdvance(); return; } if (isAutoPlay) { scheduleAutoPlayCycle(); } }, 510); // 500ms for fade-out + small buffer // Preload the next image preloadNextImage(); }; tempImg.onerror = function() { clearTimeout(loaderTimeout); loader.style.display = 'none'; isGalleryLoading = false; pendingAutoAdvance = false; if (isAutoPlay) { scheduleAutoPlayCycle(); } console.error('Error loading image:', fullUrl); }; } function preloadNextImage() { if (currentGalleryIndex < currentGalleryImages.length - 1) { const nextRelUrl = currentGalleryImages[currentGalleryIndex + 1]; const nextImage = new Image(); nextImage.onload = function () { refreshBrowserThumbnailIfMissing(nextRelUrl); }; nextImage.src = '/media/' + nextRelUrl; } } function closeGalleryModal() { clearTimeout(navHideTimer); document.querySelectorAll('.gallery-nav').forEach(btn => btn.classList.remove('nav-hidden')); const modal = document.getElementById('gallery-modal'); const stage = document.getElementById('gallery-stage'); if (modal) { modal.style.display = 'none'; modal.setAttribute('aria-hidden', 'true'); } isGalleryLoading = false; setAutoPlay(false); updateDownloadLink(null); // remove all images if (stage) { const existingImages = stage.querySelectorAll('img'); existingImages.forEach(img => { if (img.parentNode) { img.parentNode.removeChild(img); } }); } const filenameEl = document.getElementById('gallery-filename'); if (filenameEl) { const filenameTextEl = filenameEl.querySelector('.filename-text'); if (filenameTextEl) { filenameTextEl.textContent = ''; } else { filenameEl.textContent = ''; } filenameEl.dataset.filename = ''; filenameEl.style.setProperty('--filename-progress', '0%'); } const positionEl = document.getElementById('gallery-position'); if (positionEl) { positionEl.textContent = ''; } resetExifPanel(true); resetImageTransformState(); // Restore scrolling. document.body.style.overflow = ''; } function navigateByScreenHalf(clientX) { if (isGalleryLoading) { return; } const midpoint = window.innerWidth / 2; if (clientX < midpoint) { if (currentGalleryIndex > 0) { currentGalleryIndex--; showGalleryImage(currentGalleryImages[currentGalleryIndex]); } return; } if (currentGalleryIndex < currentGalleryImages.length - 1) { currentGalleryIndex++; showGalleryImage(currentGalleryImages[currentGalleryIndex]); } else { closeGalleryModal(); } } function handleGalleryScreenClick(e) { // If a pinch zoom occurred, don't change the image. if (pinchZoomOccurred) { pinchZoomOccurred = false; return; } if (currentScale > 1.01 || isPinching || isPanning) { return; } const target = e.target; if (!target) { return; } // Do not interpret clicks on controls/panels as navigation. if (target.closest('.gallery-control') || target.closest('.gallery-nav') || target.closest('#gallery-exif')) { return; } navigateByScreenHalf(e.clientX); } function initGallerySwipe() { let touchStartX = 0; let touchEndX = 0; const galleryModal = document.getElementById('gallery-modal'); if (!galleryModal) { return; } galleryModal.addEventListener('touchstart', function(e) { // If more than one finger is on screen, ignore swipe tracking. if (e.touches.length > 1) { return; } if (currentScale > 1.01 || isPinching || isPanning) { return; } touchStartX = e.changedTouches[0].screenX; }, false); galleryModal.addEventListener('touchend', function(e) { // If the event was part of a multi-touch (pinch) gesture or a pinch was detected, skip swipe. if (e.changedTouches.length > 1 || isPinching || isPanning || pinchZoomOccurred || currentScale > 1.01) { return; } touchEndX = e.changedTouches[0].screenX; handleSwipe(); }, false); function handleSwipe() { const deltaX = touchEndX - touchStartX; if (Math.abs(deltaX) > 50) { // Swipe threshold. if (deltaX > 0) { // Swipe right: previous image. if (!isGalleryLoading && currentGalleryIndex > 0) { currentGalleryIndex--; showGalleryImage(currentGalleryImages[currentGalleryIndex]); } } else { // Swipe left: next image. if (!isGalleryLoading && currentGalleryIndex < currentGalleryImages.length - 1) { currentGalleryIndex++; showGalleryImage(currentGalleryImages[currentGalleryIndex]); } } } } } function clearAutoPlayTimer() { if (autoPlayTimer) { clearTimeout(autoPlayTimer); autoPlayTimer = null; } } function setAutoPlay(active) { const playButton = document.getElementById('gallery-play'); isAutoPlay = active; if (playButton) { playButton.textContent = active ? '\u2225' : '\u25B6'; playButton.setAttribute('aria-pressed', active ? 'true' : 'false'); playButton.setAttribute('aria-label', active ? 'Pause slideshow' : 'Start slideshow'); playButton.classList.toggle('playing', active); } pendingAutoAdvance = false; clearAutoPlayTimer(); if (active) { scheduleAutoPlayCycle(); } else { setPlayRingProgress(0); } } function setPlayRingProgress(deg) { const playButton = document.getElementById('gallery-play'); if (playButton) { playButton.style.setProperty('--play-progress', `${deg}deg`); } } function restartPlayAnimation() { const playButton = document.getElementById('gallery-play'); if (!playButton) { return; } playButton.classList.remove('playing-anim'); // force reflow to restart animation void playButton.offsetWidth; setPlayRingProgress(0); playButton.classList.add('playing-anim'); } function completePlayRing() { const playButton = document.getElementById('gallery-play'); if (!playButton) { return; } playButton.classList.remove('playing-anim'); setPlayRingProgress(360); } function scheduleAutoPlayCycle() { if (!isAutoPlay) { return; } clearAutoPlayTimer(); restartPlayAnimation(); autoPlayTimer = setTimeout(() => { triggerAutoAdvance(); }, AUTO_PLAY_INTERVAL_MS); } function triggerAutoAdvance() { completePlayRing(); if (isGalleryLoading) { pendingAutoAdvance = true; return; } attemptAutoAdvance(); } function attemptAutoAdvance() { pendingAutoAdvance = false; if (!isAutoPlay) { return; } if (currentGalleryIndex >= currentGalleryImages.length - 1) { setAutoPlay(false); return; } const nextUrl = currentGalleryImages[currentGalleryIndex + 1]; const img = new Image(); img.onload = function() { if (!isAutoPlay) { return; } currentGalleryIndex++; showGalleryImage(nextUrl); }; img.onerror = function() { if (isAutoPlay) { scheduleAutoPlayCycle(); } }; img.src = '/media/' + nextUrl; } function handlePinchStart(e) { if (e.touches.length === 2) { e.preventDefault(); isPinching = true; isPanning = false; pinchZoomOccurred = false; // Reset at the start of a pinch gesture. const dx = e.touches[0].pageX - e.touches[1].pageX; const dy = e.touches[0].pageY - e.touches[1].pageY; initialPinchDistance = Math.sqrt(dx * dx + dy * dy); initialScale = currentScale; pinchStartMidX = (e.touches[0].pageX + e.touches[1].pageX) / 2; pinchStartMidY = (e.touches[0].pageY + e.touches[1].pageY) / 2; pinchStartTranslateX = currentTranslateX; pinchStartTranslateY = currentTranslateY; return; } if (e.touches.length === 1 && currentScale > 1.01) { e.preventDefault(); isPanning = true; panStartX = e.touches[0].pageX; panStartY = e.touches[0].pageY; panOriginX = currentTranslateX; panOriginY = currentTranslateY; showGalleryNav(); } } function handlePinchMove(e) { if (e.touches.length === 2 && initialPinchDistance) { e.preventDefault(); const dx = e.touches[0].pageX - e.touches[1].pageX; const dy = e.touches[0].pageY - e.touches[1].pageY; const currentDistance = Math.sqrt(dx * dx + dy * dy); let scale = currentDistance / initialPinchDistance; scale = Math.max(1, Math.min(scale * initialScale, 4)); // If the scale has changed enough, mark that a pinch zoom occurred. if (Math.abs(scale - currentScale) > 0.01) { pinchZoomOccurred = true; } currentScale = scale; const midX = (e.touches[0].pageX + e.touches[1].pageX) / 2; const midY = (e.touches[0].pageY + e.touches[1].pageY) / 2; currentTranslateX = pinchStartTranslateX + (midX - pinchStartMidX); currentTranslateY = pinchStartTranslateY + (midY - pinchStartMidY); applyImageTransform(e.currentTarget); if (currentScale > 1.01) { showGalleryNav(); } else { hideGalleryNav(); } return; } if (e.touches.length === 1 && isPanning && currentScale > 1.01) { e.preventDefault(); const deltaX = e.touches[0].pageX - panStartX; const deltaY = e.touches[0].pageY - panStartY; currentTranslateX = panOriginX + deltaX; currentTranslateY = panOriginY + deltaY; pinchZoomOccurred = true; applyImageTransform(e.currentTarget); showGalleryNav(); } } function handlePinchEnd(e) { if (e.touches.length === 1 && currentScale > 1.01) { // Transition from pinch to one-finger pan. isPinching = false; isPanning = true; initialPinchDistance = null; panStartX = e.touches[0].pageX; panStartY = e.touches[0].pageY; panOriginX = currentTranslateX; panOriginY = currentTranslateY; showGalleryNav(); return; } if (e.touches.length < 1) { isPinching = false; isPanning = false; initialPinchDistance = null; initialScale = currentScale; const image = e.currentTarget; if (currentScale <= 1.01) { currentScale = 1; currentTranslateX = 0; currentTranslateY = 0; applyImageTransform(image); hideGalleryNav(); return; } applyImageTransform(image); showGalleryNav(); } } function handleGalleryKeydown(e) { const modal = document.getElementById('gallery-modal'); if (!modal || modal.style.display !== 'flex') { return; } if (e.key === 'Escape') { closeGalleryModal(); return; } if (e.key === 'ArrowLeft') { e.preventDefault(); if (!isGalleryLoading && currentGalleryIndex > 0) { currentGalleryIndex--; showGalleryImage(currentGalleryImages[currentGalleryIndex]); } return; } if (e.key === 'ArrowRight') { e.preventDefault(); if (!isGalleryLoading && currentGalleryIndex < currentGalleryImages.length - 1) { currentGalleryIndex++; showGalleryImage(currentGalleryImages[currentGalleryIndex]); } else if (!isGalleryLoading) { closeGalleryModal(); } return; } if (e.key === ' ') { e.preventDefault(); setAutoPlay(!isAutoPlay); } } function initGalleryControls() { const nextButton = document.querySelector('.gallery-next'); const prevButton = document.querySelector('.gallery-prev'); const closeButton = document.getElementById('gallery-close'); const infoButton = document.getElementById('gallery-info'); const playButton = document.getElementById('gallery-play'); if (nextButton) { nextButton.addEventListener('click', function () { if (isGalleryLoading) { return; } if (currentGalleryIndex < currentGalleryImages.length - 1) { currentGalleryIndex++; showGalleryImage(currentGalleryImages[currentGalleryIndex]); } else { closeGalleryModal(); } }); } if (prevButton) { prevButton.addEventListener('click', function () { if (isGalleryLoading) { return; } if (currentGalleryIndex > 0) { currentGalleryIndex--; showGalleryImage(currentGalleryImages[currentGalleryIndex]); } }); } if (closeButton) { closeButton.addEventListener('click', closeGalleryModal); } const downloadButton = document.getElementById('gallery-download'); if (downloadButton) { downloadButton.addEventListener('click', async function (e) { e.preventDefault(); e.stopPropagation(); const relUrl = downloadButton.dataset.relurl; if (!relUrl) return; const encodedSubpath = relUrl .replace(/^\/+/, '') .split('/') .map(segment => encodeURIComponent(decodeURIComponent(segment))) .join('/'); // Fetch a short-lived download token (works in Telegram's internal browser) let tokenizedUrl; try { const resp = await fetch(`/create_dltoken/${encodedSubpath}`); if (!resp.ok) return; tokenizedUrl = (await resp.text()).trim(); } catch { return; } if (!tokenizedUrl) return; // Append download=true so the server returns the full-size original as attachment const sep = tokenizedUrl.includes('?') ? '&' : '?'; const downloadUrl = new URL(tokenizedUrl + sep + 'download=true', window.location.href); const a = document.createElement('a'); a.href = downloadUrl.toString(); document.body.appendChild(a); a.click(); document.body.removeChild(a); }); } if (infoButton) { infoButton.addEventListener('click', function (e) { e.stopPropagation(); toggleExifPanel(); }); } if (playButton) { playButton.addEventListener('click', function (e) { e.stopPropagation(); setAutoPlay(!isAutoPlay); }); } } function initGallery() { const galleryModal = document.getElementById('gallery-modal'); if (!galleryModal || galleryModal.dataset.galleryBound) return; initGallerySwipe(); initGalleryControls(); galleryModal.addEventListener('click', handleGalleryScreenClick); document.addEventListener('keydown', handleGalleryKeydown); galleryModal.dataset.galleryBound = '1'; } window.initGallery = initGallery;