2025-03-18 17:26:48 +00:00

274 lines
10 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
// 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 = [];
// Render breadcrumbs, directories (grid view when appropriate), and files.
function renderContent(data) {
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>&gt;</span>`;
}
});
document.getElementById('breadcrumbs').innerHTML = breadcrumbHTML;
let contentHTML = '';
// Render directories.
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.
if (data.files.length > 0) {
contentHTML += '<ul>';
data.files.forEach((file, idx) => {
let symbol = '📄';
if (file.file_type === 'music') {
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 += `<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>';
}
document.getElementById('content').innerHTML = contentHTML;
// 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;
fetch(apiUrl)
.then(response => response.json())
.then(data => {
renderContent(data);
})
.catch(error => {
console.error('Error loading directory:', error);
document.getElementById('content').innerHTML = '<p>Error loading directory.</p>';
});
}
// 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 : '/');
});
});
// 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.replace(/\//g, ' > ') + '<br><span style="font-size: larger; font-weight: bold;">' + fileName.replace('.mp3', '') + '</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.
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);
});
// 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();
}
}
});