improve user experience
This commit is contained in:
parent
1e38b00ef6
commit
8b99d538fc
150
static/app.js
150
static/app.js
@ -1,3 +1,7 @@
|
|||||||
|
// Define global variables to track music files and the current index.
|
||||||
|
let currentMusicFiles = []; // Array of objects with at least { path, index }
|
||||||
|
let currentMusicIndex = -1; // Index of the current music file
|
||||||
|
|
||||||
// Helper function: decode each segment then re-encode to avoid double encoding.
|
// Helper function: decode each segment then re-encode to avoid double encoding.
|
||||||
function encodeSubpath(subpath) {
|
function encodeSubpath(subpath) {
|
||||||
if (!subpath) return '';
|
if (!subpath) return '';
|
||||||
@ -46,15 +50,19 @@ function renderContent(data) {
|
|||||||
// Render files.
|
// Render files.
|
||||||
if (data.files.length > 0) {
|
if (data.files.length > 0) {
|
||||||
contentHTML += '<ul>';
|
contentHTML += '<ul>';
|
||||||
data.files.forEach(file => {
|
data.files.forEach((file, idx) => {
|
||||||
let symbol = '';
|
let symbol = '📄';
|
||||||
if (file.file_type === 'music') {
|
if (file.file_type === 'music') {
|
||||||
symbol = '🎵';
|
symbol = '🔊';
|
||||||
|
// Add each music file to currentMusicFiles with its index.
|
||||||
|
currentMusicFiles.push({ path: file.path, index: idx });
|
||||||
} else if (file.file_type === 'image') {
|
} else if (file.file_type === 'image') {
|
||||||
symbol = '🖼️';
|
symbol = '🖼️';
|
||||||
}
|
}
|
||||||
|
// Add a data-index attribute for music files.
|
||||||
|
const indexAttr = file.file_type === 'music' ? ` data-index="${currentMusicFiles.length - 1}"` : '';
|
||||||
contentHTML += `<li class="file-item">
|
contentHTML += `<li class="file-item">
|
||||||
<a href="#" class="play-file" data-url="${file.path}" data-file-type="${file.file_type}">${symbol} ${file.name}</a>`;
|
<a href="#" class="play-file"${indexAttr} data-url="${file.path}" data-file-type="${file.file_type}">${symbol} ${file.name}</a>`;
|
||||||
if (file.has_transcript) {
|
if (file.has_transcript) {
|
||||||
contentHTML += `<a href="#" class="show-transcript" data-url="${file.transcript_url}" title="Show Transcript">📄</a>`;
|
contentHTML += `<a href="#" class="show-transcript" data-url="${file.transcript_url}" title="Show Transcript">📄</a>`;
|
||||||
}
|
}
|
||||||
@ -64,7 +72,6 @@ function renderContent(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('content').innerHTML = contentHTML;
|
document.getElementById('content').innerHTML = contentHTML;
|
||||||
|
|
||||||
// Update global variable for gallery images (only image files).
|
// Update global variable for gallery images (only image files).
|
||||||
currentGalleryImages = data.files
|
currentGalleryImages = data.files
|
||||||
.filter(f => f.file_type === 'image')
|
.filter(f => f.file_type === 'image')
|
||||||
@ -88,6 +95,21 @@ function loadDirectory(subpath) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// preload the next audio file
|
||||||
|
function preload_audio() {
|
||||||
|
// Prefetch the next file by triggering the backend diskcache.
|
||||||
|
if (currentMusicIndex >= 0 && currentMusicIndex < currentMusicFiles.length - 1) {
|
||||||
|
const nextFile = currentMusicFiles[currentMusicIndex + 1];
|
||||||
|
const nextMediaUrl = '/media/' + nextFile.path;
|
||||||
|
// Use a HEAD request so that the backend reads and caches the file without returning the full content.
|
||||||
|
fetch(nextMediaUrl, { method: 'HEAD' })
|
||||||
|
.then(response => {
|
||||||
|
console.log('Backend diskcache initiated for next file:', nextFile.path);
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error initiating backend diskcache for next file:', error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Attach event listeners for directory, breadcrumb, file, and transcript links.
|
// Attach event listeners for directory, breadcrumb, file, and transcript links.
|
||||||
function attachEventListeners() {
|
function attachEventListeners() {
|
||||||
// Directory link clicks.
|
// Directory link clicks.
|
||||||
@ -110,40 +132,80 @@ function attachEventListeners() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// File play link clicks.
|
// Global variable to store the current fetch's AbortController.
|
||||||
document.querySelectorAll('.play-file').forEach(link => {
|
let currentFetchController = null;
|
||||||
link.addEventListener('click', function (event) {
|
|
||||||
event.preventDefault();
|
document.querySelectorAll('.play-file').forEach(link => {
|
||||||
const fileType = this.getAttribute('data-file-type');
|
link.addEventListener('click', async function (event) {
|
||||||
const relUrl = this.getAttribute('data-url');
|
event.preventDefault();
|
||||||
if (fileType === 'music') {
|
const fileType = this.getAttribute('data-file-type');
|
||||||
const mediaUrl = '/media/' + relUrl;
|
const relUrl = this.getAttribute('data-url');
|
||||||
// Check if the media URL is forbidden
|
const nowPlayingInfo = document.getElementById('nowPlayingInfo');
|
||||||
fetch(mediaUrl, { method: 'HEAD' })
|
|
||||||
.then(response => {
|
// Remove the class from all file items.
|
||||||
if (response.status === 403) {
|
document.querySelectorAll('.file-item').forEach(item => {
|
||||||
// Redirect to the root if a 403 status is returned
|
item.classList.remove('currently-playing');
|
||||||
window.location.href = '/';
|
|
||||||
} else {
|
|
||||||
// Otherwise, play the audio
|
|
||||||
const audioPlayer = document.getElementById('globalAudio');
|
|
||||||
audioPlayer.src = mediaUrl;
|
|
||||||
audioPlayer.load();
|
|
||||||
audioPlayer.play();
|
|
||||||
document.getElementById('nowPlayingInfo').textContent = relUrl;
|
|
||||||
document.querySelector('footer').style.display = 'flex';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error fetching media:', error);
|
|
||||||
// Optionally handle network errors here
|
|
||||||
});
|
|
||||||
} else if (fileType === 'image') {
|
|
||||||
// Open the gallery modal for image files
|
|
||||||
openGalleryModal(relUrl);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (fileType === 'music') {
|
||||||
|
// Update currentMusicIndex based on the clicked element.
|
||||||
|
const idx = this.getAttribute('data-index');
|
||||||
|
const audioPlayer = document.getElementById('globalAudio');
|
||||||
|
currentMusicIndex = idx !== null ? parseInt(idx) : -1;
|
||||||
|
|
||||||
|
// Add the class to the clicked file item's parent.
|
||||||
|
this.closest('.file-item').classList.add('currently-playing');
|
||||||
|
|
||||||
|
// Abort any previous fetch if it is still running.
|
||||||
|
if (currentFetchController) {
|
||||||
|
currentFetchController.abort();
|
||||||
|
}
|
||||||
|
// Create a new AbortController for the current fetch.
|
||||||
|
currentFetchController = new AbortController();
|
||||||
|
|
||||||
|
audioPlayer.pause();
|
||||||
|
audioPlayer.src = ''; // Clear existing source.
|
||||||
|
nowPlayingInfo.textContent = "Wird geladen...";
|
||||||
|
document.querySelector('footer').style.display = 'flex';
|
||||||
|
|
||||||
|
const mediaUrl = '/media/' + relUrl;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Pass the signal to the fetch so it can be aborted.
|
||||||
|
const response = await fetch(mediaUrl, { method: 'HEAD', signal: currentFetchController.signal });
|
||||||
|
|
||||||
|
if (response.status === 403) {
|
||||||
|
nowPlayingInfo.textContent = "Fehler: Zugriff verweigert.";
|
||||||
|
window.location.href = '/'; // Redirect if forbidden.
|
||||||
|
} else if (!response.ok) {
|
||||||
|
nowPlayingInfo.textContent = `Fehler: Unerwarteter Status (${response.status}).`;
|
||||||
|
console.error('Unexpected response status:', response.status);
|
||||||
|
} else {
|
||||||
|
audioPlayer.src = mediaUrl;
|
||||||
|
audioPlayer.load();
|
||||||
|
audioPlayer.play();
|
||||||
|
const pathParts = relUrl.split('/');
|
||||||
|
const fileName = pathParts.pop();
|
||||||
|
const pathStr = pathParts.join('/');
|
||||||
|
nowPlayingInfo.innerHTML = pathStr + '<br><span style="font-size: larger; font-weight: bold;">' + fileName + '</span>';
|
||||||
|
preload_audio();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If the fetch was aborted, error.name will be 'AbortError'.
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
console.log('Previous fetch aborted.');
|
||||||
|
} else {
|
||||||
|
console.error('Error fetching media:', error);
|
||||||
|
nowPlayingInfo.textContent = "Fehler: Netzwerkproblem oder ungültige URL.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (fileType === 'image') {
|
||||||
|
// Open the gallery modal for image files.
|
||||||
|
openGalleryModal(relUrl);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// Transcript icon clicks.
|
// Transcript icon clicks.
|
||||||
document.querySelectorAll('.show-transcript').forEach(link => {
|
document.querySelectorAll('.show-transcript').forEach(link => {
|
||||||
@ -194,3 +256,17 @@ window.addEventListener('popstate', function (event) {
|
|||||||
const subpath = event.state ? event.state.subpath : '';
|
const subpath = event.state ? event.state.subpath : '';
|
||||||
loadDirectory(subpath);
|
loadDirectory(subpath);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Add an event listener for the audio 'ended' event to auto-play the next music file.
|
||||||
|
document.getElementById('globalAudio').addEventListener('ended', () => {
|
||||||
|
// Check if there's a next file in the array.
|
||||||
|
if (currentMusicIndex >= 0 && currentMusicIndex < currentMusicFiles.length - 1) {
|
||||||
|
const nextFile = currentMusicFiles[currentMusicIndex + 1];
|
||||||
|
// Find the corresponding play link (or directly trigger playback).
|
||||||
|
const nextLink = document.querySelector(`.play-file[data-url="${nextFile.path}"]`);
|
||||||
|
if (nextLink) {
|
||||||
|
nextLink.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -1,204 +1,203 @@
|
|||||||
// Assume that currentGalleryImages is already declared in app.js.
|
// Assume that currentGalleryImages is already declared in app.js.
|
||||||
// Only declare currentGalleryIndex if it isn't defined already.
|
// Only declare currentGalleryIndex if it isn't defined already.
|
||||||
if (typeof currentGalleryIndex === "undefined") {
|
if (typeof currentGalleryIndex === "undefined") {
|
||||||
var currentGalleryIndex = -1;
|
var currentGalleryIndex = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openGalleryModal(relUrl) {
|
function openGalleryModal(relUrl) {
|
||||||
document.body.style.overflow = 'hidden'; // Disable background scrolling.
|
document.body.style.overflow = 'hidden'; // Disable background scrolling.
|
||||||
currentGalleryIndex = currentGalleryImages.indexOf(relUrl);
|
currentGalleryIndex = currentGalleryImages.indexOf(relUrl);
|
||||||
showGalleryImage(relUrl);
|
showGalleryImage(relUrl);
|
||||||
document.getElementById('gallery-modal').style.display = 'flex';
|
document.getElementById('gallery-modal').style.display = 'flex';
|
||||||
}
|
}
|
||||||
|
|
||||||
function showGalleryImage(relUrl) {
|
function showGalleryImage(relUrl) {
|
||||||
const fullUrl = '/media/' + relUrl;
|
const fullUrl = '/media/' + relUrl;
|
||||||
const modal = document.getElementById('gallery-modal');
|
const modal = document.getElementById('gallery-modal');
|
||||||
const currentImage = document.getElementById('gallery-modal-content');
|
const currentImage = document.getElementById('gallery-modal-content');
|
||||||
const loader = document.getElementById('gallery-loader');
|
const loader = document.getElementById('gallery-loader');
|
||||||
const closeBtn = document.getElementById('gallery-close');
|
const closeBtn = document.getElementById('gallery-close');
|
||||||
|
|
||||||
// Set a timeout for showing the loader after 500ms.
|
// Set a timeout for showing the loader after 500ms.
|
||||||
let loaderTimeout = setTimeout(() => {
|
let loaderTimeout = setTimeout(() => {
|
||||||
loader.style.display = 'block';
|
loader.style.display = 'block';
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
// Preload the new image.
|
// Preload the new image.
|
||||||
const tempImg = new Image();
|
const tempImg = new Image();
|
||||||
tempImg.src = fullUrl;
|
tempImg.src = fullUrl;
|
||||||
|
|
||||||
tempImg.onload = function () {
|
tempImg.onload = function () {
|
||||||
clearTimeout(loaderTimeout);
|
|
||||||
loader.style.display = 'none';
|
|
||||||
|
|
||||||
// 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';
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 510); // 500ms for fade-out + small buffer
|
|
||||||
|
|
||||||
// Preload the next image
|
|
||||||
preloadNextImage();
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
tempImg.onerror = function() {
|
|
||||||
clearTimeout(loaderTimeout);
|
clearTimeout(loaderTimeout);
|
||||||
loader.style.display = 'none';
|
loader.style.display = 'none';
|
||||||
console.error('Error loading image:', fullUrl);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function preloadNextImage() {
|
// Select all current images in the gallery modal
|
||||||
if (currentGalleryIndex < currentGalleryImages.length - 1) {
|
const existingImages = document.querySelectorAll('#gallery-modal img');
|
||||||
const nextImage = new Image();
|
|
||||||
nextImage.src = '/media/' + currentGalleryImages[currentGalleryIndex + 1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeGalleryModal() {
|
// Create a new image element for the transition
|
||||||
document.getElementById('gallery-modal').style.display = 'none';
|
const newImage = document.createElement('img');
|
||||||
document.body.style.overflow = ''; // Restore scrolling.
|
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';
|
||||||
|
|
||||||
function handleGalleryImageClick(e) {
|
// Append new image on top of existing ones
|
||||||
if (e.target.classList.contains('gallery-next') || e.target.classList.contains('gallery-prev')) {
|
modal.appendChild(newImage);
|
||||||
return; // Prevent conflict
|
|
||||||
}
|
|
||||||
|
|
||||||
const imgRect = this.getBoundingClientRect();
|
// Fade out all old images and fade in the new one
|
||||||
if (e.clientX < imgRect.left + imgRect.width / 2) {
|
setTimeout(() => {
|
||||||
if (currentGalleryIndex > 0) {
|
existingImages.forEach(img => {
|
||||||
currentGalleryIndex--;
|
img.style.opacity = 0; // Apply fade-out
|
||||||
showGalleryImage(currentGalleryImages[currentGalleryIndex]);
|
});
|
||||||
}
|
newImage.style.opacity = 1; // Fade in the new image
|
||||||
} else {
|
}, 10);
|
||||||
if (currentGalleryIndex < currentGalleryImages.length - 1) {
|
|
||||||
currentGalleryIndex++;
|
// Wait for fade-out to finish, then remove all old images
|
||||||
showGalleryImage(currentGalleryImages[currentGalleryIndex]);
|
setTimeout(() => {
|
||||||
}
|
existingImages.forEach(img => {
|
||||||
}
|
if (img.parentNode) {
|
||||||
|
img.parentNode.removeChild(img); // Remove old images
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 510); // 500ms for fade-out + small buffer
|
||||||
|
|
||||||
|
// Preload the next image
|
||||||
|
preloadNextImage();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
tempImg.onerror = function() {
|
||||||
|
clearTimeout(loaderTimeout);
|
||||||
|
loader.style.display = 'none';
|
||||||
|
console.error('Error loading image:', fullUrl);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function preloadNextImage() {
|
||||||
|
if (currentGalleryIndex < currentGalleryImages.length - 1) {
|
||||||
|
const nextImage = new Image();
|
||||||
function initGallerySwipe() {
|
nextImage.src = '/media/' + currentGalleryImages[currentGalleryIndex + 1];
|
||||||
let touchStartX = 0;
|
|
||||||
let touchEndX = 0;
|
|
||||||
const galleryModal = document.getElementById('gallery-modal');
|
|
||||||
|
|
||||||
galleryModal.addEventListener('touchstart', function(e) {
|
|
||||||
touchStartX = e.changedTouches[0].screenX;
|
|
||||||
}, false);
|
|
||||||
|
|
||||||
galleryModal.addEventListener('touchend', function(e) {
|
|
||||||
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 (currentGalleryIndex > 0) {
|
|
||||||
currentGalleryIndex--;
|
|
||||||
showGalleryImage(currentGalleryImages[currentGalleryIndex]);
|
|
||||||
}
|
|
||||||
} else { // Swipe left: next image.
|
|
||||||
if (currentGalleryIndex < currentGalleryImages.length - 1) {
|
|
||||||
currentGalleryIndex++;
|
|
||||||
showGalleryImage(currentGalleryImages[currentGalleryIndex]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function initGalleryControls() {
|
|
||||||
const nextButton = document.querySelector('.gallery-next');
|
|
||||||
const prevButton = document.querySelector('.gallery-prev');
|
|
||||||
const closeButton = document.getElementById('gallery-close');
|
|
||||||
|
|
||||||
if (nextButton) {
|
|
||||||
nextButton.addEventListener('click', function () {
|
|
||||||
if (currentGalleryIndex < currentGalleryImages.length - 1) {
|
|
||||||
currentGalleryIndex++;
|
|
||||||
showGalleryImage(currentGalleryImages[currentGalleryIndex]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevButton) {
|
|
||||||
prevButton.addEventListener('click', function () {
|
|
||||||
if (currentGalleryIndex > 0) {
|
|
||||||
currentGalleryIndex--;
|
|
||||||
showGalleryImage(currentGalleryImages[currentGalleryIndex]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (closeButton) {
|
|
||||||
closeButton.addEventListener('click', closeGalleryModal);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeGalleryModal() {
|
||||||
|
document.getElementById('gallery-modal').style.display = 'none';
|
||||||
|
document.body.style.overflow = ''; // Restore scrolling.
|
||||||
|
}
|
||||||
|
|
||||||
function initGallery() {
|
function handleGalleryImageClick(e) {
|
||||||
const galleryImage = document.getElementById('gallery-modal-content');
|
if (e.target.classList.contains('gallery-next') || e.target.classList.contains('gallery-prev')) {
|
||||||
if (galleryImage) {
|
return; // Prevent conflict
|
||||||
galleryImage.addEventListener('click', handleGalleryImageClick);
|
|
||||||
}
|
|
||||||
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.
|
const imgRect = this.getBoundingClientRect();
|
||||||
if (document.readyState === "loading") {
|
if (e.clientX < imgRect.left + imgRect.width / 2) {
|
||||||
document.addEventListener('DOMContentLoaded', initGallery);
|
if (currentGalleryIndex > 0) {
|
||||||
|
currentGalleryIndex--;
|
||||||
|
showGalleryImage(currentGalleryImages[currentGalleryIndex]);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
initGallery();
|
if (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) {
|
||||||
|
touchStartX = e.changedTouches[0].screenX;
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
galleryModal.addEventListener('touchend', function(e) {
|
||||||
|
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 (currentGalleryIndex > 0) {
|
||||||
|
currentGalleryIndex--;
|
||||||
|
showGalleryImage(currentGalleryImages[currentGalleryIndex]);
|
||||||
|
}
|
||||||
|
} else { // Swipe left: next image.
|
||||||
|
if (currentGalleryIndex < currentGalleryImages.length - 1) {
|
||||||
|
currentGalleryIndex++;
|
||||||
|
showGalleryImage(currentGalleryImages[currentGalleryIndex]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function initGalleryControls() {
|
||||||
|
const nextButton = document.querySelector('.gallery-next');
|
||||||
|
const prevButton = document.querySelector('.gallery-prev');
|
||||||
|
const closeButton = document.getElementById('gallery-close');
|
||||||
|
|
||||||
|
if (nextButton) {
|
||||||
|
nextButton.addEventListener('click', function () {
|
||||||
|
if (currentGalleryIndex < currentGalleryImages.length - 1) {
|
||||||
|
currentGalleryIndex++;
|
||||||
|
showGalleryImage(currentGalleryImages[currentGalleryIndex]);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prevButton) {
|
||||||
|
prevButton.addEventListener('click', function () {
|
||||||
|
if (currentGalleryIndex > 0) {
|
||||||
|
currentGalleryIndex--;
|
||||||
|
showGalleryImage(currentGalleryImages[currentGalleryIndex]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closeButton) {
|
||||||
|
closeButton.addEventListener('click', closeGalleryModal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function initGallery() {
|
||||||
|
const galleryImage = document.getElementById('gallery-modal-content');
|
||||||
|
if (galleryImage) {
|
||||||
|
galleryImage.addEventListener('click', handleGalleryImageClick);
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|||||||
@ -1,26 +1,36 @@
|
|||||||
|
/* Ensure html and body take full height */
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Global Styles */
|
/* Global Styles */
|
||||||
body {
|
body {
|
||||||
font-family: 'Helvetica Neue', Arial, sans-serif;
|
font-family: 'Helvetica Neue', Arial, sans-serif;
|
||||||
background-color: #f4f7f9;
|
background-color: #f4f7f9;
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
.wrapper {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
.container {
|
.container {
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 20px;
|
padding: 10px;
|
||||||
padding-bottom: 150px;
|
padding-bottom: 200px;
|
||||||
}
|
}
|
||||||
h1 {
|
|
||||||
text-align: center;
|
.container a {
|
||||||
margin-bottom: 10px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Breadcrumb Styles */
|
/* Breadcrumb Styles */
|
||||||
.breadcrumb {
|
.breadcrumb {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
font-size: 16px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
.breadcrumb a {
|
.breadcrumb a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@ -47,6 +57,7 @@ li {
|
|||||||
|
|
||||||
/* Directory Items (in a list) */
|
/* Directory Items (in a list) */
|
||||||
.directory-item {
|
.directory-item {
|
||||||
|
padding: 15px;
|
||||||
/* Use flex for list items if needed */
|
/* Use flex for list items if needed */
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +69,7 @@ li {
|
|||||||
}
|
}
|
||||||
.directories-grid .directory-item {
|
.directories-grid .directory-item {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
padding: 10px;
|
padding: 15px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||||
@ -70,7 +81,7 @@ li {
|
|||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
padding: 10px;
|
padding: 15px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||||
@ -80,7 +91,6 @@ li {
|
|||||||
a.directory-link,
|
a.directory-link,
|
||||||
a.play-file {
|
a.play-file {
|
||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
font-weight: bold;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
@ -98,6 +108,10 @@ a.show-transcript:hover {
|
|||||||
color: #d35400;
|
color: #d35400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.currently-playing {
|
||||||
|
background-color: #a6bedf; /* Adjust to your preferred color */
|
||||||
|
}
|
||||||
|
|
||||||
/* Footer Player Styles */
|
/* Footer Player Styles */
|
||||||
footer {
|
footer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -106,32 +120,30 @@ footer {
|
|||||||
right: 0;
|
right: 0;
|
||||||
background-color: #34495e;
|
background-color: #34495e;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 10px;
|
padding: 7px;
|
||||||
|
text-align: center;
|
||||||
|
z-index: 1000; /* Make sure it sits on top */
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
footer audio {
|
||||||
|
width: 75%;
|
||||||
|
scale: 1.3;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
.audio-player-container {
|
.audio-player-container {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||||
padding: 10px;
|
padding: 7px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 800px;
|
|
||||||
}
|
}
|
||||||
footer audio {
|
|
||||||
width: 100%;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
outline: none;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Now Playing Info */
|
/* Now Playing Info */
|
||||||
.now-playing-info {
|
.now-playing-info {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
font-size: 14px;
|
font-size: 16px;
|
||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@ -183,28 +195,3 @@ footer audio {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive Adjustments */
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.container {
|
|
||||||
padding: 10px;
|
|
||||||
padding-bottom: 150px;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
.breadcrumb {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
.mp3-item {
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
}
|
|
||||||
a.show-transcript {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
footer {
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 15px 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -25,21 +25,23 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="wrapper">
|
||||||
<h2>Gottesdienste Speyer und Schwegenheim</h2>
|
<div class="container">
|
||||||
<div id="breadcrumbs" class="breadcrumb"></div>
|
<h2>Gottesdienste Speyer und Schwegenheim</h2>
|
||||||
<div id="content"></div>
|
<div id="breadcrumbs" class="breadcrumb"></div>
|
||||||
</div>
|
<div id="content"></div>
|
||||||
|
|
||||||
<!-- Global Audio Player in Footer -->
|
|
||||||
<footer style="display: none;">
|
|
||||||
<div class="audio-player-container">
|
|
||||||
<audio id="globalAudio" controls>
|
|
||||||
Your browser does not support the audio element.
|
|
||||||
</audio>
|
|
||||||
<div id="nowPlayingInfo" class="now-playing-info"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
|
||||||
|
<!-- Global Audio Player in Footer -->
|
||||||
|
<footer style="display: none;">
|
||||||
|
<div class="audio-player-container">
|
||||||
|
<audio id="globalAudio" controls preload="auto">
|
||||||
|
Your browser does not support the audio element.
|
||||||
|
</audio>
|
||||||
|
<div id="nowPlayingInfo" class="now-playing-info"></div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Transcript Modal -->
|
<!-- Transcript Modal -->
|
||||||
<div id="transcriptModal">
|
<div id="transcriptModal">
|
||||||
@ -64,7 +66,7 @@
|
|||||||
<script>
|
<script>
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
navigator.serviceWorker.register('{{ url_for('static', filename='sw.js') }}')
|
navigator.serviceWorker.register('{{ url_for("static", filename="sw.js") }}')
|
||||||
.then(reg => console.log('Service worker registered.', reg))
|
.then(reg => console.log('Service worker registered.', reg))
|
||||||
.catch(err => console.error('Service worker not registered.', err));
|
.catch(err => console.error('Service worker not registered.', err));
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user