// 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 = ""; let activeTranscriptAudio = ""; // Audio file that matches the currently open transcript. // Check thumb availability in parallel; sequentially generate missing ones to avoid concurrent creation. function ensureFirstFivePreviews() { const images = Array.from(document.querySelectorAll('.images-grid img')).slice(0, 5); if (!images.length) return; const checks = images.map(img => { const thumbUrl = img.dataset.thumbUrl || img.src; const fullUrl = img.dataset.fullUrl; return fetch(thumbUrl, { method: 'GET', cache: 'no-store' }) .then(res => ({ img, thumbUrl, fullUrl, hasThumb: res.ok && res.status !== 204 })) .catch(() => ({ img, thumbUrl, fullUrl, hasThumb: false })); }); Promise.all(checks).then(results => { const missing = results.filter(r => !r.hasThumb && r.fullUrl); if (!missing.length) return; generateThumbsSequentially(missing); }); } async function generateThumbsSequentially(entries) { for (const { img, thumbUrl, fullUrl } of entries) { try { await fetch(fullUrl, { method: 'GET', cache: 'no-store' }); const cacheBust = `_=${Date.now()}`; const separator = thumbUrl.includes('?') ? '&' : '?'; img.src = `${thumbUrl}${separator}${cacheBust}`; } catch (e) { // ignore; best effort } } } // 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('/'); } // Convert a timecode string like "00:17" or "01:02:03" into total seconds. function parseTimecode(text) { const parts = text.trim().split(':').map(part => parseInt(part, 10)); if (parts.some(Number.isNaN) || parts.length < 2 || parts.length > 3) return null; const hasHours = parts.length === 3; const [hours, minutes, seconds] = hasHours ? parts : [0, parts[0], parts[1]]; if (seconds < 0 || seconds > 59) return null; if (minutes < 0 || (hasHours && minutes > 59)) return null; if (hours < 0) return null; return hours * 3600 + minutes * 60 + seconds; } // Add click handlers to transcript timecodes so they seek the matching audio. function attachTranscriptTimecodes(audioPath) { const container = document.getElementById('transcriptContent'); const targetAudio = audioPath || activeTranscriptAudio; if (!container || !targetAudio) return; container.querySelectorAll('code').forEach(codeEl => { if (codeEl.closest('pre')) return; const label = codeEl.textContent.trim(); const seconds = parseTimecode(label); if (seconds === null) return; codeEl.classList.add('timecode-link'); codeEl.title = `Zu ${label} springen`; codeEl.addEventListener('click', () => jumpToTimecode(seconds, targetAudio)); }); } // Ensure we load the right track (if needed) and jump to the requested time. function jumpToTimecode(seconds, relUrl) { if (!relUrl || seconds === null) return; const updateTrackState = () => { currentTrackPath = relUrl; const matchIdx = currentMusicFiles.findIndex(file => file.path === relUrl); currentMusicIndex = matchIdx !== -1 ? matchIdx : -1; }; const seekWhenReady = () => { player.audio.currentTime = Math.min( seconds, Number.isFinite(player.audio.duration) ? player.audio.duration : seconds ); player.audio.play(); player.updateTimeline(); }; updateTrackState(); if (player.currentRelUrl === relUrl) { if (player.audio.readyState >= 1) { seekWhenReady(); } else { player.audio.addEventListener('loadedmetadata', seekWhenReady, { once: true }); } return; } player.audio.addEventListener('loadedmetadata', seekWhenReady, { once: true }); const trackLink = document.querySelector(`.play-file[data-url="${relUrl}"]`); if (trackLink) { trackLink.click(); } else { player.loadTrack(relUrl); } } // Global variable for gallery images (updated from current folder) let currentGalleryImages = []; // Fallback thumbnail (camera icon) when thumbnail generation is unavailable const thumbnailFallbackData = 'data:image/svg+xml;utf8,' + encodeURIComponent( `` ); function handleThumbnailError(imgEl) { imgEl.onerror = null; // Prevent infinite loop imgEl.src = thumbnailFallbackData; imgEl.classList.add('thumbnail-fallback'); const item = imgEl.closest('.image-item'); if (item) { item.classList.add('loaded'); } } function handleThumbnailLoad(imgEl) { // Some servers return 204 for missing thumbs, which triggers load with zero dimensions if (!imgEl.naturalWidth || !imgEl.naturalHeight) { handleThumbnailError(imgEl); return; } const item = imgEl.closest('.image-item'); if (item) { item.classList.add('loaded'); } } 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 += `${crumb.name}`; if (index < data.breadcrumbs.length - 1) { breadcrumbHTML += `>`; } }); document.getElementById('breadcrumbs').innerHTML = breadcrumbHTML; const imageFiles = data.files.filter(file => file.file_type === 'image'); const nonImageFiles = data.files.filter(file => file.file_type !== 'image'); // Check for image-only directory (no subdirectories, at least one file, all images) const isImageOnly = data.directories.length === 0 && imageFiles.length > 0 && nonImageFiles.length === 0; let contentHTML = ''; if (isImageOnly) { // Display thumbnails grid contentHTML += '
Error loading directory!
${error}
`; 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' } }); } } const TRACK_CLICK_DEBOUNCE_MS = 3000; let lastTrackClick = { url: null, ts: 0 }; // 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; const now = Date.now(); if (fileType === 'music') { // If this is the same track already loaded, ignore to avoid extra GETs and unselects. if (player.currentRelUrl === relUrl) { return; } // Remove the class from all file items. document.querySelectorAll('.file-item').forEach(item => { item.classList.remove('currently-playing'); }); // Debounce repeated clicks on the same track to avoid extra GETs. if (lastTrackClick.url === relUrl && now - lastTrackClick.ts < TRACK_CLICK_DEBOUNCE_MS) { return; } lastTrackClick = { url: relUrl, ts: now }; // Update the current music index. currentMusicIndex = index !== undefined ? parseInt(index) : -1; currentTrackPath = relUrl; // Mark the clicked item as currently playing. this.closest('.file-item').classList.add('currently-playing'); const reqId = crypto.randomUUID ? crypto.randomUUID() : (Date.now().toString(36) + Math.random().toString(36).slice(2)); player.loadTrack(relUrl, reqId); // 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 const reqId = crypto.randomUUID ? crypto.randomUUID() : (Date.now().toString(36) + Math.random().toString(36).slice(2)); const urlWithReq = `/media/${relUrl}${relUrl.includes('?') ? '&' : '?'}req=${encodeURIComponent(reqId)}`; window.location.href = urlWithReq; } }); }); // Transcript icon clicks. document.querySelectorAll('.show-transcript').forEach(link => { link.addEventListener('click', function (event) { event.preventDefault(); const url = this.getAttribute('data-url'); const audioUrl = this.getAttribute('data-audio-url') || ''; const highlight = this.getAttribute('highlight'); activeTranscriptAudio = audioUrl; 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, '$1'); } } document.getElementById('transcriptContent').innerHTML = marked.parse(data); attachTranscriptTimecodes(audioUrl); document.getElementById('transcriptModal').style.display = 'block'; }) .catch(error => { activeTranscriptAudio = ''; document.getElementById('transcriptContent').innerHTML = 'Error loading transcription.
'; document.getElementById('transcriptModal').style.display = 'block'; }); }); }); // create token icon clicks. document.querySelectorAll('.create-share').forEach(link => { link.addEventListener('click', function (event) { event.preventDefault(); const url = '/create_token/' + 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 = "You can't share this.
"; 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