improve user experience

This commit is contained in:
lelo 2025-03-17 17:09:30 +00:00
parent 1e38b00ef6
commit 8b99d538fc
5 changed files with 349 additions and 285 deletions

View File

@ -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">&#128196;</a>`; contentHTML += `<a href="#" class="show-transcript" data-url="${file.transcript_url}" title="Show Transcript">&#128196;</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) {
document.querySelectorAll('.play-file').forEach(link => {
link.addEventListener('click', async function (event) {
event.preventDefault(); event.preventDefault();
const fileType = this.getAttribute('data-file-type'); const fileType = this.getAttribute('data-file-type');
const relUrl = this.getAttribute('data-url'); const relUrl = this.getAttribute('data-url');
const nowPlayingInfo = document.getElementById('nowPlayingInfo');
// Remove the class from all file items.
document.querySelectorAll('.file-item').forEach(item => {
item.classList.remove('currently-playing');
});
if (fileType === 'music') { if (fileType === 'music') {
const mediaUrl = '/media/' + relUrl; // Update currentMusicIndex based on the clicked element.
// Check if the media URL is forbidden const idx = this.getAttribute('data-index');
fetch(mediaUrl, { method: 'HEAD' })
.then(response => {
if (response.status === 403) {
// Redirect to the root if a 403 status is returned
window.location.href = '/';
} else {
// Otherwise, play the audio
const audioPlayer = document.getElementById('globalAudio'); 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.src = mediaUrl;
audioPlayer.load(); audioPlayer.load();
audioPlayer.play(); audioPlayer.play();
document.getElementById('nowPlayingInfo').textContent = relUrl; const pathParts = relUrl.split('/');
document.querySelector('footer').style.display = 'flex'; 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) {
.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); console.error('Error fetching media:', error);
// Optionally handle network errors here nowPlayingInfo.textContent = "Fehler: Netzwerkproblem oder ungültige URL.";
}); }
}
} else if (fileType === 'image') { } else if (fileType === 'image') {
// Open the gallery modal for image files // Open the gallery modal for image files.
openGalleryModal(relUrl); 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();
}
}
});

View File

@ -2,16 +2,16 @@
// 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');
@ -77,21 +77,21 @@ if (typeof currentGalleryIndex === "undefined") {
loader.style.display = 'none'; loader.style.display = 'none';
console.error('Error loading image:', fullUrl); console.error('Error loading image:', fullUrl);
}; };
} }
function preloadNextImage() { function preloadNextImage() {
if (currentGalleryIndex < currentGalleryImages.length - 1) { if (currentGalleryIndex < currentGalleryImages.length - 1) {
const nextImage = new Image(); const nextImage = new Image();
nextImage.src = '/media/' + currentGalleryImages[currentGalleryIndex + 1]; nextImage.src = '/media/' + currentGalleryImages[currentGalleryIndex + 1];
} }
} }
function closeGalleryModal() { function closeGalleryModal() {
document.getElementById('gallery-modal').style.display = 'none'; document.getElementById('gallery-modal').style.display = 'none';
document.body.style.overflow = ''; // Restore scrolling. document.body.style.overflow = ''; // Restore scrolling.
} }
function handleGalleryImageClick(e) { function handleGalleryImageClick(e) {
if (e.target.classList.contains('gallery-next') || e.target.classList.contains('gallery-prev')) { if (e.target.classList.contains('gallery-next') || e.target.classList.contains('gallery-prev')) {
return; // Prevent conflict return; // Prevent conflict
} }
@ -113,7 +113,7 @@ if (typeof currentGalleryIndex === "undefined") {
function initGallerySwipe() { function initGallerySwipe() {
let touchStartX = 0; let touchStartX = 0;
let touchEndX = 0; let touchEndX = 0;
const galleryModal = document.getElementById('gallery-modal'); const galleryModal = document.getElementById('gallery-modal');
@ -143,12 +143,12 @@ if (typeof currentGalleryIndex === "undefined") {
} }
} }
} }
} }
function initGalleryControls() { function initGalleryControls() {
const nextButton = document.querySelector('.gallery-next'); const nextButton = document.querySelector('.gallery-next');
const prevButton = document.querySelector('.gallery-prev'); const prevButton = document.querySelector('.gallery-prev');
const closeButton = document.getElementById('gallery-close'); const closeButton = document.getElementById('gallery-close');
@ -177,7 +177,7 @@ if (typeof currentGalleryIndex === "undefined") {
} }
function initGallery() { function initGallery() {
const galleryImage = document.getElementById('gallery-modal-content'); const galleryImage = document.getElementById('gallery-modal-content');
if (galleryImage) { if (galleryImage) {
galleryImage.addEventListener('click', handleGalleryImageClick); galleryImage.addEventListener('click', handleGalleryImageClick);
@ -193,12 +193,11 @@ if (typeof currentGalleryIndex === "undefined") {
} }
}); });
} }
// Initialize the gallery once the DOM content is loaded. // Initialize the gallery once the DOM content is loaded.
if (document.readyState === "loading") { if (document.readyState === "loading") {
document.addEventListener('DOMContentLoaded', initGallery); document.addEventListener('DOMContentLoaded', initGallery);
} else { } else {
initGallery(); initGallery();
} }

View File

@ -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;
}
}

View File

@ -25,6 +25,7 @@
<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="wrapper">
<div class="container"> <div class="container">
<h2>Gottesdienste Speyer und Schwegenheim</h2> <h2>Gottesdienste Speyer und Schwegenheim</h2>
<div id="breadcrumbs" class="breadcrumb"></div> <div id="breadcrumbs" class="breadcrumb"></div>
@ -34,12 +35,13 @@
<!-- Global Audio Player in Footer --> <!-- Global Audio Player in Footer -->
<footer style="display: none;"> <footer style="display: none;">
<div class="audio-player-container"> <div class="audio-player-container">
<audio id="globalAudio" controls> <audio id="globalAudio" controls preload="auto">
Your browser does not support the audio element. Your browser does not support the audio element.
</audio> </audio>
<div id="nowPlayingInfo" class="now-playing-info"></div> <div id="nowPlayingInfo" class="now-playing-info"></div>
</div> </div>
</footer> </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));
}); });