2025-05-29 12:38:11 +00:00

537 lines
19 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 = "";
// 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');
// 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}"
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 gehö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>`;
}
contentHTML += `<li class="directory-item"><a href="#" class="directory-link" data-path="${dir.path}">📁 ${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">&#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();
}
// 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');
startPlaying(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);
});
}
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);