2025-04-21 14:08:23 +02:00

500 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.

// 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) {
const fileItem = currentMusicFileElement.closest('.file-item');
fileItem.classList.add('currently-playing');
// setTimeout(() => {
// fileItem.scrollIntoView({ block: "center", inline: "nearest" });
// }, 300);
}
}
}
}
function renderContent(data) {
// Render breadcrumbs
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">&#8203;&gt;</span>`;
}
});
document.getElementById('breadcrumbs').innerHTML = breadcrumbHTML;
// Check for image-only directory (no subdirectories, at least one file, all images)
const isImageOnly = data.directories.length === 0
&& data.files.length > 0
&& data.files.every(file => file.file_type === 'image');
let contentHTML = '';
if (isImageOnly) {
// Display thumbnails grid
contentHTML += '<div class="images-grid">';
data.files.forEach(file => {
const thumbUrl = `${file.path}?thumbnail=true`;
contentHTML += `
<div class="image-item">
<a href="#" class="play-file image-link" data-url="${file.path}" data-file-type="${file.file_type}">
<img src="/media/${thumbUrl}" alt="${file.name}" class="thumbnail" />
</a>
</div>
`;
});
contentHTML += '</div>';
} else {
// Render directories normally
if (data.directories.length > 0) {
const areAllShort = data.directories.every(dir => dir.name.length <= 15) && data.files.length === 0;
if (areAllShort && data.breadcrumbs.length !== 1) {
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>`;
});
if (data.breadcrumbs.length === 1) {
contentHTML += `<li class="link-item" onclick="window.location.href='/search'">🔎 <a href="/search" class="link-link">Suche</a></li>`;
}
contentHTML += '</ul>';
}
}
// Render files (including music and non-image files)
currentMusicFiles = [];
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">&#128196;</a>`;
}
contentHTML += `</li>`;
});
contentHTML += '</ul>';
}
}
// Insert generated content
document.getElementById('content').innerHTML = contentHTML;
// Attach event listeners for directories
document.querySelectorAll('.directory-item').forEach(item => {
item.addEventListener('click', function(e) {
if (!e.target.closest('a')) {
const anchor = item.querySelector('a.directory-link');
if (anchor) anchor.click();
}
});
});
// Attach event listeners for file items (including images)
if (isImageOnly) {
document.querySelectorAll('.image-item').forEach(item => {
item.addEventListener('click', function(e) {
if (!e.target.closest('a')) {
const anchor = item.querySelector('a.image-link');
if (anchor) anchor.click();
}
});
});
} else {
document.querySelectorAll('li.file-item').forEach(item => {
item.addEventListener('click', function(e) {
if (!e.target.closest('a')) {
const anchor = item.querySelector('a.play-file');
if (anchor) anchor.click();
}
});
});
}
// Update gallery images for lightbox or similar
currentGalleryImages = data.files
.filter(f => f.file_type === 'image')
.map(f => f.path);
attachEventListeners();
}
// 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);
if (data.playfile) {
const playFileLink = document.querySelector(`.play-file[data-url="${data.playfile}"]`);
if (playFileLink) {
playFileLink.click();
}
}
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',
headers: {
'X-Cache-Request': 'true'
}
});
}
}
// 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: '/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);
} else {
// serve like a download
window.location.href = `/media/${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);
// });
function syncThemeColor() {
const cssVar = getComputedStyle(document.documentElement)
.getPropertyValue('--dark-background')
.trim();
if (!cssVar) return;
// sync the themecolor meta tag
document.querySelector('meta[name="theme-color"]')
.setAttribute('content', cssVar);
// apply fill to every <svg> inside elements with .icon-color
document
.querySelectorAll('.icon-color svg')
.forEach(svg => svg.setAttribute('fill', cssVar));
}
document.addEventListener('DOMContentLoaded', syncThemeColor);