bethaus-app/static/gallery.js
2026-01-17 19:41:50 +00:00

619 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

let isPinching = false;
let pinchZoomOccurred = false;
let initialPinchDistance = null;
let initialScale = 1;
let currentScale = 1;
let exifAbortController = null;
let isGalleryLoading = false;
let autoPlayTimer = null;
let isAutoPlay = false;
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;
function encodeGalleryPath(relUrl) {
if (!relUrl) {
return '';
}
return relUrl
.split('/')
.map(segment => encodeURIComponent(segment))
.join('/');
}
function escapeHtml(value) {
return String(value === undefined || value === null ? '' : value).replace(/[&<>"']/g, (char) => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[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 = '<div class="exif-row exif-placeholder">No EXIF data</div>';
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 = '<div class="exif-row exif-placeholder">No EXIF data</div>';
return;
}
exifPanel.innerHTML = rows.map(([key, value]) => {
const safeKey = escapeHtml(key);
const safeValue = escapeHtml(Array.isArray(value) ? value.join(', ') : value);
return `<div class="exif-row"><span class="exif-key">${safeKey}</span><span class="exif-value">${safeValue}</span></div>`;
}).join('');
}
function showExifPlaceholder(message) {
const exifPanel = document.getElementById('gallery-exif');
if (!exifPanel) {
return;
}
exifPanel.innerHTML = `<div class="exif-row exif-placeholder">${message}</div>`;
}
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;
}
if (!relUrl) {
filenameEl.textContent = '';
return;
}
const parts = relUrl.split('/');
const filename = parts[parts.length - 1] || relUrl;
filenameEl.textContent = filename;
}
function updateNextButtonState() {
const nextButton = document.querySelector('.gallery-next');
if (!nextButton) {
return;
}
const atLastImage = currentGalleryIndex >= currentGalleryImages.length - 1;
nextButton.textContent = atLastImage ? 'x' : '';
nextButton.setAttribute('aria-label', atLastImage ? 'Close gallery' : 'Next image');
}
function openGalleryModal(relUrl) {
document.body.style.overflow = 'hidden'; // Disable background scrolling.
currentGalleryIndex = currentGalleryImages.indexOf(relUrl);
showGalleryImage(relUrl);
document.getElementById('gallery-modal').style.display = 'flex';
}
function showGalleryImage(relUrl) {
if (isGalleryLoading) {
return;
}
isGalleryLoading = true;
clearAutoPlayTimer();
updateNextButtonState();
loadExifData(relUrl);
const fullUrl = '/media/' + relUrl;
const modal = document.getElementById('gallery-modal');
const currentImage = document.getElementById('gallery-modal-content');
const loader = document.getElementById('loader-container');
const closeBtn = document.getElementById('gallery-close');
// 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);
// Select all current images in the gallery modal
const existingImages = document.querySelectorAll('#gallery-modal 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.maxWidth = '95vw'; // Prevent overflow on large screens
newImage.style.maxHeight = '95vh'; // Prevent overflow on tall screens
newImage.style.transition = 'opacity 0.5s ease-in-out';
newImage.style.opacity = 0; // Start fully transparent
newImage.id = 'gallery-modal-content';
// 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('click', handleGalleryImageClick);
// Reset pinch variables for the new image
initialPinchDistance = null;
initialScale = 1;
currentScale = 1;
pinchZoomOccurred = false;
// Append new image on top of existing ones
modal.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 nextImage = new Image();
nextImage.src = '/media/' + currentGalleryImages[currentGalleryIndex + 1];
}
}
function closeGalleryModal() {
document.getElementById('gallery-modal').style.display = 'none';
isGalleryLoading = false;
setAutoPlay(false);
// remove all images
const existingImages = document.querySelectorAll('#gallery-modal img');
existingImages.forEach(img => {
if (img.parentNode) {
img.parentNode.removeChild(img);
}
});
const filenameEl = document.getElementById('gallery-filename');
if (filenameEl) {
filenameEl.textContent = '';
}
resetExifPanel(true);
// Restore scrolling.
document.body.style.overflow = '';
}
function handleGalleryImageClick(e) {
// If a pinch zoom occurred, don't change the image.
if (pinchZoomOccurred) {
pinchZoomOccurred = false; // Reset the flag for the next gesture.
e.stopPropagation();
return;
}
if (e.target.classList.contains('gallery-next') || e.target.classList.contains('gallery-prev')) {
return; // Prevent conflict with navigation buttons.
}
const imgRect = this.getBoundingClientRect();
if (e.clientX < imgRect.left + imgRect.width / 2) {
if (!isGalleryLoading && currentGalleryIndex > 0) {
currentGalleryIndex--;
showGalleryImage(currentGalleryImages[currentGalleryIndex]);
}
} else {
if (!isGalleryLoading && currentGalleryIndex < currentGalleryImages.length - 1) {
currentGalleryIndex++;
showGalleryImage(currentGalleryImages[currentGalleryIndex]);
}
}
}
function initGallerySwipe() {
let touchStartX = 0;
let touchEndX = 0;
const galleryModal = document.getElementById('gallery-modal');
galleryModal.addEventListener('touchstart', function(e) {
// If more than one finger is on screen, ignore swipe tracking.
if (e.touches.length > 1) {
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 || pinchZoomOccurred) {
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) {
clearInterval(autoPlayTimer);
autoPlayTimer = null;
}
}
function setAutoPlay(active) {
const playButton = document.getElementById('gallery-play');
isAutoPlay = active;
if (playButton) {
playButton.textContent = active ? '||' : '>';
playButton.setAttribute('aria-pressed', active ? 'true' : 'false');
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;
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);
}
}
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(0.5, Math.min(scale * initialScale, 3));
// If the scale has changed enough, mark that a pinch zoom occurred.
if (Math.abs(scale - initialScale) > 0.05) {
pinchZoomOccurred = true;
}
currentScale = scale;
e.currentTarget.style.transform = `translate(-50%, -50%) scale(${scale})`;
}
}
function handlePinchEnd(e) {
if (e.touches.length < 2) {
initialScale = currentScale;
initialPinchDistance = null;
isPinching = false;
}
}
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);
}
if (infoButton) {
infoButton.addEventListener('click', function (e) {
e.stopPropagation();
toggleExifPanel();
});
}
if (playButton) {
playButton.addEventListener('click', function (e) {
e.stopPropagation();
setAutoPlay(!isAutoPlay);
});
}
}
function initGallery() {
initGallerySwipe();
initGalleryControls();
// Close modal when clicking outside the image.
const galleryModal = document.getElementById('gallery-modal');
galleryModal.addEventListener('click', function(e) {
if (e.target === galleryModal) {
closeGalleryModal();
}
});
}
// Initialize the gallery once the DOM content is loaded.
if (document.readyState === "loading") {
document.addEventListener('DOMContentLoaded', initGallery);
} else {
initGallery();
}