diff --git a/app.py b/app.py
index 861a5bf..da364ef 100755
--- a/app.py
+++ b/app.py
@@ -246,4 +246,4 @@ def index(path):
return render_template("browse.html")
if __name__ == "__main__":
- app.run(debug=True, host='0.0.0.0')
+ app.run(debug=True, host='0.0.0.0')
\ No newline at end of file
diff --git a/static/app.js b/static/app.js
index 2f8cf58..2ca52e0 100644
--- a/static/app.js
+++ b/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.
function encodeSubpath(subpath) {
if (!subpath) return '';
@@ -46,15 +50,19 @@ function renderContent(data) {
// Render files.
if (data.files.length > 0) {
contentHTML += '
';
- data.files.forEach(file => {
- let symbol = '';
+ data.files.forEach((file, idx) => {
+ let symbol = '📄';
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') {
symbol = '🖼️';
}
+ // Add a data-index attribute for music files.
+ const indexAttr = file.file_type === 'music' ? ` data-index="${currentMusicFiles.length - 1}"` : '';
contentHTML += `
- ${symbol} ${file.name}`;
+ ${symbol} ${file.name}`;
if (file.has_transcript) {
contentHTML += `📄`;
}
@@ -64,7 +72,6 @@ function renderContent(data) {
}
document.getElementById('content').innerHTML = contentHTML;
-
// Update global variable for gallery images (only image files).
currentGalleryImages = data.files
.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.
function attachEventListeners() {
// Directory link clicks.
@@ -110,40 +132,80 @@ function attachEventListeners() {
});
});
- // File play link clicks.
- document.querySelectorAll('.play-file').forEach(link => {
- link.addEventListener('click', function (event) {
- event.preventDefault();
- const fileType = this.getAttribute('data-file-type');
- const relUrl = this.getAttribute('data-url');
- if (fileType === 'music') {
- const mediaUrl = '/media/' + relUrl;
- // Check if the media URL is forbidden
- 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');
- 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);
- }
+// Global variable to store the current fetch's AbortController.
+let currentFetchController = null;
+
+document.querySelectorAll('.play-file').forEach(link => {
+ link.addEventListener('click', async function (event) {
+ event.preventDefault();
+ const fileType = this.getAttribute('data-file-type');
+ 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') {
+ // 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 + ' ' + fileName + '';
+ 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.
document.querySelectorAll('.show-transcript').forEach(link => {
@@ -194,3 +256,17 @@ window.addEventListener('popstate', function (event) {
const subpath = event.state ? event.state.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();
+ }
+ }
+});
\ No newline at end of file
diff --git a/static/gallery.js b/static/gallery.js
index 3ea3200..ab891da 100644
--- a/static/gallery.js
+++ b/static/gallery.js
@@ -1,204 +1,203 @@
// 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;
- }
+ var currentGalleryIndex = -1;
+}
+
+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) {
+ const fullUrl = '/media/' + relUrl;
+ const modal = document.getElementById('gallery-modal');
+ const currentImage = document.getElementById('gallery-modal-content');
+ const loader = document.getElementById('gallery-loader');
+ const closeBtn = document.getElementById('gallery-close');
- 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) {
- const fullUrl = '/media/' + relUrl;
- const modal = document.getElementById('gallery-modal');
- const currentImage = document.getElementById('gallery-modal-content');
- const loader = document.getElementById('gallery-loader');
- const closeBtn = document.getElementById('gallery-close');
-
- // Set a timeout for showing the loader after 500ms.
- let loaderTimeout = setTimeout(() => {
- loader.style.display = 'block';
- }, 500);
-
- // Preload the new image.
- const tempImg = new Image();
- tempImg.src = fullUrl;
-
- 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() {
+ // Set a timeout for showing the loader after 500ms.
+ let loaderTimeout = setTimeout(() => {
+ loader.style.display = 'block';
+ }, 500);
+
+ // Preload the new image.
+ const tempImg = new Image();
+ tempImg.src = fullUrl;
+
+ tempImg.onload = function () {
clearTimeout(loaderTimeout);
loader.style.display = 'none';
- console.error('Error loading image:', fullUrl);
- };
- }
- function preloadNextImage() {
- if (currentGalleryIndex < currentGalleryImages.length - 1) {
- const nextImage = new Image();
- nextImage.src = '/media/' + currentGalleryImages[currentGalleryIndex + 1];
- }
- }
+ // Select all current images in the gallery modal
+ const existingImages = document.querySelectorAll('#gallery-modal img');
- function closeGalleryModal() {
- document.getElementById('gallery-modal').style.display = 'none';
- document.body.style.overflow = ''; // Restore scrolling.
- }
+ // 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();
+ };
- function handleGalleryImageClick(e) {
- if (e.target.classList.contains('gallery-next') || e.target.classList.contains('gallery-prev')) {
- return; // Prevent conflict
- }
- const imgRect = this.getBoundingClientRect();
- if (e.clientX < imgRect.left + imgRect.width / 2) {
- if (currentGalleryIndex > 0) {
- currentGalleryIndex--;
- showGalleryImage(currentGalleryImages[currentGalleryIndex]);
- }
- } else {
- if (currentGalleryIndex < currentGalleryImages.length - 1) {
- currentGalleryIndex++;
- showGalleryImage(currentGalleryImages[currentGalleryIndex]);
- }
- }
+ tempImg.onerror = function() {
+ clearTimeout(loaderTimeout);
+ loader.style.display = 'none';
+ console.error('Error loading image:', fullUrl);
+ };
}
-
-
-
- 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 preloadNextImage() {
+ if (currentGalleryIndex < currentGalleryImages.length - 1) {
+ const nextImage = new Image();
+ nextImage.src = '/media/' + currentGalleryImages[currentGalleryIndex + 1];
}
-
-
-
-
- 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() {
- 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();
- }
- });
-
+function handleGalleryImageClick(e) {
+ if (e.target.classList.contains('gallery-next') || e.target.classList.contains('gallery-prev')) {
+ return; // Prevent conflict
}
-
- // Initialize the gallery once the DOM content is loaded.
- if (document.readyState === "loading") {
- document.addEventListener('DOMContentLoaded', initGallery);
+
+ const imgRect = this.getBoundingClientRect();
+ if (e.clientX < imgRect.left + imgRect.width / 2) {
+ if (currentGalleryIndex > 0) {
+ currentGalleryIndex--;
+ showGalleryImage(currentGalleryImages[currentGalleryIndex]);
+ }
} else {
- initGallery();
+ if (currentGalleryIndex < currentGalleryImages.length - 1) {
+ currentGalleryIndex++;
+ showGalleryImage(currentGalleryImages[currentGalleryIndex]);
+ }
}
-
\ No newline at end of file
+}
+
+
+
+
+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();
+}
diff --git a/static/styles.css b/static/styles.css
index afd8561..6c3db35 100644
--- a/static/styles.css
+++ b/static/styles.css
@@ -1,26 +1,36 @@
+/* Ensure html and body take full height */
+html, body {
+ height: 100%;
+ margin: 0;
+}
+
/* Global Styles */
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
background-color: #f4f7f9;
- margin: 0;
padding: 0;
color: #333;
}
+.wrapper {
+ display: grid;
+ grid-template-rows: auto 1fr auto;
+ min-height: 100%;
+}
.container {
max-width: 900px;
margin: 0 auto;
- padding: 20px;
- padding-bottom: 150px;
+ padding: 10px;
+ padding-bottom: 200px;
}
-h1 {
- text-align: center;
- margin-bottom: 10px;
+
+.container a {
+ font-size: 18px;
}
/* Breadcrumb Styles */
.breadcrumb {
margin-bottom: 20px;
- font-size: 16px;
+ font-size: 18px;
}
.breadcrumb a {
text-decoration: none;
@@ -47,6 +57,7 @@ li {
/* Directory Items (in a list) */
.directory-item {
+ padding: 15px;
/* Use flex for list items if needed */
}
@@ -58,7 +69,7 @@ li {
}
.directories-grid .directory-item {
background-color: #fff;
- padding: 10px;
+ padding: 15px;
border-radius: 5px;
text-align: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
@@ -70,7 +81,7 @@ li {
grid-template-columns: 1fr auto;
align-items: center;
margin: 10px 0;
- padding: 10px;
+ padding: 15px;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
@@ -80,7 +91,6 @@ li {
a.directory-link,
a.play-file {
color: #2c3e50;
- font-weight: bold;
text-decoration: none;
word-break: break-all;
}
@@ -98,6 +108,10 @@ a.show-transcript:hover {
color: #d35400;
}
+.currently-playing {
+ background-color: #a6bedf; /* Adjust to your preferred color */
+}
+
/* Footer Player Styles */
footer {
position: fixed;
@@ -106,32 +120,30 @@ footer {
right: 0;
background-color: #34495e;
color: #fff;
- padding: 10px;
+ padding: 7px;
+ text-align: center;
+ z-index: 1000; /* Make sure it sits on top */
display: flex;
align-items: center;
justify-content: center;
}
+footer audio {
+ width: 75%;
+ scale: 1.3;
+ padding: 10px 0;
+}
.audio-player-container {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
- padding: 10px;
+ padding: 7px;
width: 100%;
- max-width: 800px;
}
-footer audio {
- width: 100%;
- border: none;
- border-radius: 8px;
- outline: none;
- background: transparent;
-}
-
/* Now Playing Info */
.now-playing-info {
margin-top: 5px;
- font-size: 14px;
+ font-size: 16px;
color: #2c3e50;
text-align: center;
}
@@ -183,28 +195,3 @@ footer audio {
padding: 10px;
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;
- }
-}
\ No newline at end of file
diff --git a/templates/browse.html b/templates/browse.html
index a2301ea..8f899de 100644
--- a/templates/browse.html
+++ b/templates/browse.html
@@ -25,21 +25,23 @@
-