502 lines
18 KiB
JavaScript
502 lines
18 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 = "";
|
||
|
||
// Cache common DOM elements
|
||
const mainContainer = document.querySelector('main');
|
||
const searchContainer = document.querySelector('search');
|
||
const footer = document.querySelector('footer');
|
||
|
||
console.log(mainContainer, searchContainer, footer);
|
||
|
||
function viewSearch() {
|
||
// Hide the main container and show the search container
|
||
mainContainer.style.display = 'none';
|
||
searchContainer.style.display = 'block';
|
||
}
|
||
|
||
function viewMain() {
|
||
// Hide the search container and show the main container
|
||
searchContainer.style.display = 'none';
|
||
mainContainer.style.display = 'block';
|
||
}
|
||
|
||
|
||
// 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');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
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">​></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}"
|
||
class="thumbnail"
|
||
onload="this.closest('.image-item').classList.add('loaded')"
|
||
/>
|
||
</a>
|
||
<span class="file-name">${file.name}</span>
|
||
</div>
|
||
`;
|
||
});
|
||
contentHTML += '</div>';
|
||
} else {
|
||
share_link = '';
|
||
// Render directories normally
|
||
if (data.directories.length > 0) {
|
||
const areAllShort = data.directories.every(dir => dir.name.length <= 10) && data.files.length === 0;
|
||
if (areAllShort && data.breadcrumbs.length !== 1) {
|
||
contentHTML += '<div class="directories-grid">';
|
||
data.directories.forEach(dir => {
|
||
if (admin_enabled && data.breadcrumbs.length != 1 && dir.share) {
|
||
share_link = `<a href="#" class="create-share" data-url="${dir.path}">⚙️</a>`;
|
||
}
|
||
contentHTML += `<div class="directory-item"><a href="#" class="directory-link" data-path="${dir.path}">📁 ${dir.name}</a>
|
||
${share_link}
|
||
</div>`;
|
||
});
|
||
contentHTML += '</div>';
|
||
} else {
|
||
contentHTML += '<ul>';
|
||
if (data.breadcrumbs.length === 1 && Array.isArray(data.folder_today) && data.folder_today.length > 0) {
|
||
contentHTML += `<li class="directory-item"><a href="#" class="directory-link" data-path="heute">📅 Heute</a></li>`;
|
||
} else if (data.breadcrumbs.length === 1 && Array.isArray(data.folder_yesterday) && data.folder_yesterday.length > 0) {
|
||
contentHTML += `<li class="directory-item"><a href="#" class="directory-link" data-path="gestern">📅 Gestern</a></li>`;
|
||
}
|
||
if (data.breadcrumbs.length === 1 ) {
|
||
contentHTML += `<li class="directory-item"><a href="#" class="directory-link" data-path="toplist">🔥 oft angehört</a></li>`;
|
||
}
|
||
console.log(data.folder_today, data.folder_yesterday);
|
||
data.directories.forEach(dir => {
|
||
if (admin_enabled && data.breadcrumbs.length != 1 && dir.share) {
|
||
share_link = `<a href="#" class="create-share" data-url="${dir.path}">⚙️</a>`;
|
||
}
|
||
if (dir.path.includes('toplist')) {
|
||
link_symbol = '⭐';
|
||
} else {
|
||
link_symbol = '📁';
|
||
}
|
||
contentHTML += `<li class="directory-item"><a href="#" class="directory-link" data-path="${dir.path}">${link_symbol} ${dir.name}</a>${share_link}</li>`;
|
||
});
|
||
if (data.breadcrumbs.length === 1) {
|
||
contentHTML += `<li class="link-item" onclick="viewSearch()"><a onclick="viewSearch() 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">📄</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();
|
||
}
|
||
|
||
// Helper function to show the spinner
|
||
function showSpinner() {
|
||
const overlay = document.getElementById('fullscreen-loader');
|
||
if (overlay) overlay.style.display = 'flex';
|
||
}
|
||
|
||
function hideSpinner() {
|
||
const overlay = document.getElementById('fullscreen-loader');
|
||
if (overlay) overlay.style.display = 'none';
|
||
}
|
||
|
||
// Fetch directory data from the API.
|
||
function loadDirectory(subpath) {
|
||
const spinnerTimer = setTimeout(showSpinner, 500);
|
||
const encodedPath = encodeSubpath(subpath);
|
||
const apiUrl = '/api/path/' + encodedPath;
|
||
return fetch(apiUrl)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
clearTimeout(spinnerTimer);
|
||
hideSpinner();
|
||
if (data.breadcrumbs) {
|
||
renderContent(data);
|
||
} else if (data.error) {
|
||
document.getElementById('content').innerHTML = `<div class="alert alert-warning">${data.error}</div>`;
|
||
return;
|
||
}
|
||
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 => {
|
||
clearTimeout(spinnerTimer);
|
||
hideSpinner();
|
||
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 : '/');
|
||
});
|
||
});
|
||
|
||
|
||
|
||
|
||
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;
|
||
|
||
// Mark the clicked item as currently playing.
|
||
this.closest('.file-item').classList.add('currently-playing');
|
||
|
||
player.loadTrack(relUrl);
|
||
|
||
// Delay preloading to avoid blocking playback.
|
||
setTimeout(preload_audio, 1000);
|
||
|
||
} 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');
|
||
const highlight = this.getAttribute('highlight');
|
||
fetch(url)
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
throw new Error('Network response was not ok');
|
||
}
|
||
return response.text();
|
||
})
|
||
.then(data => {
|
||
// Highlight words in the transcript (substring match, case-insensitive)
|
||
if (highlight) {
|
||
// Escape special regex chars
|
||
const escapeRegExp = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||
|
||
// Split on whitespace, escape each word, and join with |
|
||
const words = highlight
|
||
.trim()
|
||
.split(/\s+/)
|
||
.map(escapeRegExp)
|
||
.filter(Boolean);
|
||
|
||
if (words.length) {
|
||
const regex = new RegExp(`(${words.join('|')})`, 'gi');
|
||
data = data.replace(regex, '<span class="highlight">$1</span>');
|
||
}
|
||
}
|
||
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';
|
||
});
|
||
});
|
||
});
|
||
|
||
// create share icon clicks.
|
||
document.querySelectorAll('.create-share').forEach(link => {
|
||
link.addEventListener('click', function (event) {
|
||
event.preventDefault();
|
||
const url = '/create_share/' + this.getAttribute('data-url');
|
||
console.log(url);
|
||
fetch(url)
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
throw new Error('Network response was not ok');
|
||
}
|
||
return response.text();
|
||
})
|
||
.then(data => {
|
||
console.log(data);
|
||
document.getElementById('transcriptContent').innerHTML = data;
|
||
document.getElementById('transcriptModal').style.display = 'block';
|
||
})
|
||
.catch(error => {
|
||
console.error('Error creating share:', error);
|
||
document.getElementById('transcriptContent').innerHTML = "<p>You can't share this.</p>";
|
||
document.getElementById('transcriptModal').style.display = 'block';
|
||
});
|
||
});
|
||
});
|
||
|
||
// Handle back/forward navigation.
|
||
window.addEventListener('popstate', function (event) {
|
||
const subpath = event.state ? event.state.subpath : '';
|
||
loadDirectory(subpath);
|
||
});
|
||
|
||
} // End of attachEventListeners function
|
||
|
||
// 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);
|
||
});
|
||
|
||
|
||
|
||
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);
|
||
});
|
||
}
|
||
|
||
|
||
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 theme‑color 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);
|