454 lines
16 KiB
JavaScript
454 lines
16 KiB
JavaScript
// 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
|
|
let currentTrackPath = "";
|
|
|
|
// Helper function: decode each segment then re-encode to avoid double encoding.
|
|
function encodeSubpath(subpath) {
|
|
if (!subpath) return '';
|
|
return subpath
|
|
.split('/')
|
|
.map(segment => encodeURIComponent(decodeURIComponent(segment)))
|
|
.join('/');
|
|
}
|
|
|
|
// Global variable for gallery images (updated from current folder)
|
|
let currentGalleryImages = [];
|
|
|
|
|
|
function paintFile() {
|
|
// Highlight the currently playing file
|
|
if (currentTrackPath) {
|
|
const currentMusicFile = currentMusicFiles.find(file => file.path === currentTrackPath);
|
|
if (currentMusicFile) {
|
|
const currentMusicFileElement = document.querySelector(`.play-file[data-url="${currentMusicFile.path}"]`);
|
|
if (currentMusicFileElement) {
|
|
currentMusicFileElement.closest('.file-item').classList.add('currently-playing');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function renderContent(data) {
|
|
|
|
// Render breadcrumbs, directories (grid view when appropriate), and files.
|
|
let breadcrumbHTML = '';
|
|
data.breadcrumbs.forEach((crumb, index) => {
|
|
breadcrumbHTML += `<a href="#" class="breadcrumb-link" data-path="${crumb.path}">${crumb.name}</a>`;
|
|
if (index < data.breadcrumbs.length - 1) {
|
|
breadcrumbHTML += `<span class="separator">​></span>`;
|
|
}
|
|
});
|
|
document.getElementById('breadcrumbs').innerHTML = breadcrumbHTML;
|
|
|
|
|
|
// Render directories.
|
|
let contentHTML = '';
|
|
if (data.directories.length > 0) {
|
|
contentHTML += '<ul>';
|
|
// Check if every directory name is short (≤15 characters)
|
|
const areAllShort = data.directories.every(dir => dir.name.length <= 15);
|
|
if (areAllShort) {
|
|
contentHTML += '<div class="directories-grid">';
|
|
data.directories.forEach(dir => {
|
|
contentHTML += `<div class="directory-item">📁 <a href="#" class="directory-link" data-path="${dir.path}">${dir.name}</a></div>`;
|
|
});
|
|
contentHTML += '</div>';
|
|
} else {
|
|
contentHTML += '<ul>';
|
|
data.directories.forEach(dir => {
|
|
contentHTML += `<li class="directory-item">📁 <a href="#" class="directory-link" data-path="${dir.path}">${dir.name}</a></li>`;
|
|
});
|
|
contentHTML += '</ul>';
|
|
}
|
|
}
|
|
|
|
// Render files.
|
|
currentMusicFiles = []; // Reset the music files array.
|
|
if (data.files.length > 0) {
|
|
contentHTML += '<ul>';
|
|
data.files.forEach((file, idx) => {
|
|
let symbol = '📄';
|
|
if (file.file_type === 'music') {
|
|
symbol = '🔊';
|
|
currentMusicFiles.push({ path: file.path, index: idx, title: file.name.replace('.mp3', '') });
|
|
} else if (file.file_type === 'image') {
|
|
symbol = '🖼️';
|
|
}
|
|
const indexAttr = file.file_type === 'music' ? ` data-index="${currentMusicFiles.length - 1}"` : '';
|
|
contentHTML += `<li class="file-item">
|
|
<a href="#" class="play-file"${indexAttr} data-url="${file.path}" data-file-type="${file.file_type}">${symbol} ${file.name.replace('.mp3', '')}</a>`;
|
|
if (file.has_transcript) {
|
|
contentHTML += `<a href="#" class="show-transcript" data-url="${file.transcript_url}" title="Show Transcript">📄</a>`;
|
|
}
|
|
contentHTML += `</li>`;
|
|
});
|
|
contentHTML += '</ul>';
|
|
}
|
|
|
|
// Insert the generated HTML into a container in the DOM.
|
|
document.getElementById('content').innerHTML = contentHTML;
|
|
|
|
// Now attach the event listeners for directories.
|
|
document.querySelectorAll('div.directory-item').forEach(li => {
|
|
li.addEventListener('click', function(e) {
|
|
// Only trigger if the click did not start on an <a>.
|
|
if (!e.target.closest('a')) {
|
|
const anchor = li.querySelector('a.directory-link');
|
|
if (anchor) {
|
|
anchor.click();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Now attach the event listeners for directories.
|
|
document.querySelectorAll('li.directory-item').forEach(li => {
|
|
li.addEventListener('click', function(e) {
|
|
// Only trigger if the click did not start on an <a>.
|
|
if (!e.target.closest('a')) {
|
|
const anchor = li.querySelector('a.directory-link');
|
|
if (anchor) {
|
|
anchor.click();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Now attach the event listeners for files.
|
|
document.querySelectorAll('li.file-item').forEach(li => {
|
|
li.addEventListener('click', function(e) {
|
|
// Only trigger if the click did not start on an <a>.
|
|
if (!e.target.closest('a')) {
|
|
const anchor = li.querySelector('a.play-file');
|
|
if (anchor) {
|
|
anchor.click();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Update global variable for gallery images (only image files).
|
|
currentGalleryImages = data.files
|
|
.filter(f => f.file_type === 'image')
|
|
.map(f => f.path);
|
|
|
|
attachEventListeners(); // Reattach event listeners for newly rendered elements.
|
|
}
|
|
|
|
// Fetch directory data from the API.
|
|
function loadDirectory(subpath) {
|
|
const encodedPath = encodeSubpath(subpath);
|
|
const apiUrl = '/api/path/' + encodedPath;
|
|
return fetch(apiUrl)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
renderContent(data);
|
|
paintFile();
|
|
return data; // return data for further chaining
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading directory:', error);
|
|
document.getElementById('content').innerHTML = `<p>Error loading directory!</p><p>${error}</p>`;
|
|
throw error; // Propagate the error
|
|
});
|
|
}
|
|
|
|
// 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.
|
|
document.querySelectorAll('.directory-link').forEach(link => {
|
|
link.addEventListener('click', function (event) {
|
|
event.preventDefault();
|
|
const newPath = this.getAttribute('data-path');
|
|
loadDirectory(newPath);
|
|
history.pushState({ subpath: newPath }, '', newPath ? '/path/' + newPath : '/');
|
|
// Scroll to the top of the page after click:
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
});
|
|
});
|
|
|
|
// Breadcrumb link clicks.
|
|
document.querySelectorAll('.breadcrumb-link').forEach(link => {
|
|
link.addEventListener('click', function (event) {
|
|
event.preventDefault();
|
|
const newPath = this.getAttribute('data-path');
|
|
loadDirectory(newPath);
|
|
history.pushState({ subpath: newPath }, '', newPath ? '/path/' + newPath : '/');
|
|
});
|
|
});
|
|
|
|
// Cache common DOM elements
|
|
const nowPlayingInfo = document.getElementById('nowPlayingInfo');
|
|
const audioPlayer = document.getElementById('globalAudio');
|
|
const playerButton = document.querySelector('.player-button');
|
|
const audioPlayerContainer = document.getElementById('audioPlayerContainer');
|
|
const footer = document.querySelector('footer');
|
|
|
|
// 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, url: relUrl, index } = this.dataset;
|
|
|
|
// Remove the class from all file items.
|
|
document.querySelectorAll('.file-item').forEach(item => {
|
|
item.classList.remove('currently-playing');
|
|
});
|
|
|
|
if (fileType === 'music') {
|
|
// Update the current music index.
|
|
currentMusicIndex = index !== undefined ? parseInt(index) : -1;
|
|
|
|
// Display the audio player container.
|
|
audioPlayerContainer.style.display = "block";
|
|
|
|
// Mark the clicked item as currently playing.
|
|
this.closest('.file-item').classList.add('currently-playing');
|
|
|
|
// Abort any previous fetch if still running.
|
|
if (currentFetchController) {
|
|
currentFetchController.abort();
|
|
}
|
|
currentFetchController = new AbortController();
|
|
|
|
// Pause the audio and clear its source.
|
|
audioPlayer.pause();
|
|
audioPlayer.src = '';
|
|
|
|
// Set a timeout to display a loader message if needed.
|
|
const loaderTimeout = setTimeout(() => {
|
|
playerButton.innerHTML = playIcon;
|
|
nowPlayingInfo.textContent = "Wird geladen...";
|
|
}, 500);
|
|
|
|
footer.style.display = 'flex';
|
|
|
|
const mediaUrl = `/media/${relUrl}`;
|
|
|
|
try {
|
|
// Perform a HEAD request to verify media availability.
|
|
const response = await fetch(mediaUrl, { method: 'HEAD', signal: currentFetchController.signal });
|
|
clearTimeout(loaderTimeout);
|
|
|
|
if (response.status === 403) {
|
|
nowPlayingInfo.textContent = "Fehler: Zugriff verweigert.";
|
|
window.location.href = '/';
|
|
return;
|
|
} else if (!response.ok) {
|
|
nowPlayingInfo.textContent = `Fehler: Unerwarteter Status (${response.status}).`;
|
|
console.error('Unexpected response status:', response.status);
|
|
return;
|
|
}
|
|
|
|
// Set the media URL, load, and play the audio.
|
|
audioPlayer.src = mediaUrl;
|
|
audioPlayer.load();
|
|
await audioPlayer.play();
|
|
currentTrackPath = relUrl;
|
|
playerButton.innerHTML = pauseIcon;
|
|
|
|
// Process file path for display.
|
|
const pathParts = relUrl.split('/');
|
|
const folderName = pathParts[pathParts.length - 2];
|
|
const fileName = pathParts.pop();
|
|
const pathStr = pathParts.join('/');
|
|
|
|
// Update Media Session metadata if available.
|
|
if ('mediaSession' in navigator) {
|
|
navigator.mediaSession.metadata = new MediaMetadata({
|
|
title: currentMusicFiles[currentMusicIndex].title,
|
|
artist: folderName,
|
|
artwork: [
|
|
{ src: '/static/icons/logo-192x192.png', sizes: '192x192', type: 'image/png' }
|
|
]
|
|
});
|
|
}
|
|
|
|
nowPlayingInfo.innerHTML = pathStr.replace(/\//g, ' > ') +
|
|
'<br><span style="font-size: larger; font-weight: bold;">' +
|
|
fileName.replace('.mp3', '') + '</span>';
|
|
|
|
// Delay preloading to avoid blocking playback.
|
|
setTimeout(preload_audio, 1000);
|
|
} catch (error) {
|
|
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 => {
|
|
link.addEventListener('click', function (event) {
|
|
event.preventDefault();
|
|
const url = this.getAttribute('data-url');
|
|
fetch(url)
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Network response was not ok');
|
|
}
|
|
return response.text();
|
|
})
|
|
.then(data => {
|
|
document.getElementById('transcriptContent').innerHTML = marked.parse(data);
|
|
document.getElementById('transcriptModal').style.display = 'block';
|
|
})
|
|
.catch(error => {
|
|
document.getElementById('transcriptContent').innerHTML = '<p>Error loading transcription.</p>';
|
|
document.getElementById('transcriptModal').style.display = 'block';
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// Modal close logic for transcript modal.
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
const closeBtn = document.querySelector('#transcriptModal .close');
|
|
closeBtn.addEventListener('click', function () {
|
|
document.getElementById('transcriptModal').style.display = 'none';
|
|
});
|
|
window.addEventListener('click', function (event) {
|
|
if (event.target == document.getElementById('transcriptModal')) {
|
|
document.getElementById('transcriptModal').style.display = 'none';
|
|
}
|
|
});
|
|
|
|
// Load initial directory based on URL.
|
|
let initialSubpath = '';
|
|
if (window.location.pathname.indexOf('/path/') === 0) {
|
|
initialSubpath = window.location.pathname.substring(6); // remove "/path/"
|
|
}
|
|
loadDirectory(initialSubpath);
|
|
});
|
|
|
|
// Handle back/forward navigation.
|
|
window.addEventListener('popstate', function (event) {
|
|
const subpath = event.state ? event.state.subpath : '';
|
|
loadDirectory(subpath);
|
|
});
|
|
|
|
|
|
let isReloadButtonVisible = true; // Boolean to track the visibility of the reload button.
|
|
|
|
function reloadDirectory() {
|
|
if (!isReloadButtonVisible) return Promise.resolve();
|
|
|
|
// Extract the current path from the URL.
|
|
let currentSubpath = '';
|
|
if (window.location.pathname.indexOf('/path/') === 0) {
|
|
currentSubpath = window.location.pathname.substring(6);
|
|
}
|
|
|
|
// Return the promise so that we can chain actions after the directory has reloaded.
|
|
return loadDirectory(currentSubpath)
|
|
.then(() => {
|
|
// Animate the reload button as before.
|
|
const reloadBtn = document.querySelector('#reload-button');
|
|
const reloadBtnSVG = document.querySelector('#reload-button svg');
|
|
|
|
void reloadBtnSVG.offsetWidth; // Force reflow to reset the animation
|
|
reloadBtnSVG.classList.add("rotate");
|
|
isReloadButtonVisible = false;
|
|
|
|
setTimeout(() => {
|
|
reloadBtnSVG.classList.remove("rotate");
|
|
reloadBtn.style.transition = 'opacity 0.5s ease';
|
|
reloadBtn.style.opacity = '1';
|
|
reloadBtn.style.pointer = 'cursor';
|
|
isReloadButtonVisible = true;
|
|
}, 10000);
|
|
});
|
|
}
|
|
|
|
|
|
|
|
if ('mediaSession' in navigator) {
|
|
|
|
// Handler for the play action
|
|
navigator.mediaSession.setActionHandler('play', () => {
|
|
document.getElementById('globalAudio').play();
|
|
});
|
|
|
|
// Handler for the pause action
|
|
navigator.mediaSession.setActionHandler('pause', () => {
|
|
document.getElementById('globalAudio').pause();
|
|
});
|
|
|
|
// Handler for the previous track action
|
|
navigator.mediaSession.setActionHandler('previoustrack', () => {
|
|
if (currentMusicIndex > 0) {
|
|
const prevFile = currentMusicFiles[currentMusicIndex - 1];
|
|
const prevLink = document.querySelector(`.play-file[data-url="${prevFile.path}"]`);
|
|
if (prevLink) {
|
|
prevLink.click();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Handler for the next track action
|
|
navigator.mediaSession.setActionHandler('nexttrack', () => {
|
|
if (currentMusicIndex >= 0 && currentMusicIndex < currentMusicFiles.length - 1) {
|
|
const nextFile = currentMusicFiles[currentMusicIndex + 1];
|
|
const nextLink = document.querySelector(`.play-file[data-url="${nextFile.path}"]`);
|
|
if (nextLink) {
|
|
nextLink.click();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
document.getElementById('globalAudio').addEventListener('ended', () => {
|
|
|
|
reloadDirectory().then(() => {
|
|
|
|
// If we had a track playing, try to find it in the updated list.
|
|
if (currentTrackPath) {
|
|
const newIndex = currentMusicFiles.findIndex(file => file.path === currentTrackPath);
|
|
currentMusicIndex = newIndex;
|
|
}
|
|
|
|
// Now, if there's a next track, auto-play it.
|
|
if (currentMusicIndex >= 0 && currentMusicIndex < currentMusicFiles.length - 1) {
|
|
const nextFile = currentMusicFiles[currentMusicIndex + 1];
|
|
const nextLink = document.querySelector(`.play-file[data-url="${nextFile.path}"]`);
|
|
if (nextLink) {
|
|
nextLink.click();
|
|
}
|
|
}
|
|
}).catch(error => {
|
|
console.error('Error during reload:', error);
|
|
});
|
|
});
|
|
|
|
// document.addEventListener("DOMContentLoaded", function() {
|
|
// // Automatically reload every 5 minutes (300,000 milliseconds)
|
|
// setInterval(reloadDirectory, 300000);
|
|
// });
|