// read the CSS variable from :root (or any selector) const cssVar = getComputedStyle(document.documentElement).getPropertyValue('--dark-background').trim(); // player DOM elements const nowPlayingInfo = document.getElementById('nowPlayingInfo'); const audio = document.getElementById('globalAudio'); const audioPlayerContainer = document.getElementById('audioPlayerContainer'); const playerButton = document.querySelector('.player-button'), timeline = document.querySelector('.timeline'), timeInfo = document.getElementById('timeInfo'), playIcon = ` `, pauseIcon = ` `, soundIcon = ` ` function toggleAudio () { if (audio.paused) { audio.play(); playerButton.innerHTML = pauseIcon; } else { audio.pause(); playerButton.innerHTML = playIcon; } } playerButton.addEventListener('click', toggleAudio); let isSeeking = false; // --- Slider (Timeline) events --- // Mouse events timeline.addEventListener('mousedown', () => { isSeeking = true; }); timeline.addEventListener('mouseup', () => { isSeeking = false; changeSeek(); // Update the audio currentTime based on the slider position }); // Touch events timeline.addEventListener('touchstart', () => { isSeeking = true; }); timeline.addEventListener('touchend', () => { isSeeking = false; changeSeek(); }); timeline.addEventListener('touchcancel', () => { isSeeking = false; }); // --- When metadata is loaded, set slider range in seconds --- audio.addEventListener('loadedmetadata', () => { timeline.min = 0; timeline.max = audio.duration; timeline.value = 0; timeline.style.backgroundSize = '0% 100%'; }); audio.addEventListener('play', () => { if ('mediaSession' in navigator) navigator.mediaSession.playbackState = 'playing'; }); audio.addEventListener('pause', () => { if ('mediaSession' in navigator) navigator.mediaSession.playbackState = 'paused'; }); // --- Seek function: directly set audio.currentTime using slider's value (in seconds) --- function changeSeek() { audio.currentTime = timeline.value; } // --- Utility: Format seconds as mm:ss --- function formatTime(seconds) { const minutes = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${minutes < 10 ? '0' : ''}${minutes}:${secs < 10 ? '0' : ''}${secs}`; } // --- Update timeline, time info, and media session on each time update --- function updateTimeline() { if (!isSeeking && audio.duration) { timeline.value = audio.currentTime; timeline.style.backgroundSize = `${(audio.currentTime / audio.duration) * 100}% 100%`; timeInfo.textContent = `${formatTime(audio.currentTime)} / ${formatTime(audio.duration)}`; } } audio.ontimeupdate = updateTimeline; // Fallback for mobile throttling setInterval(() => { if (!audio.paused && !isSeeking) updateTimeline(); }, 500); // --- When audio ends --- audio.onended = function() { playerButton.innerHTML = playIcon; }; async function downloadAudio() { const src = audio.currentSrc || audio.src; if (!src) return; // Build the URL with your download flag + cache‑buster const downloadUrl = new URL(src, window.location.href); downloadUrl.searchParams.set('download', 'true'); downloadUrl.searchParams.set('_', Date.now()); // Create a “real” link to that URL and click it const a = document.createElement('a'); a.href = downloadUrl.toString(); a.download = ''; // tell Safari “this is a download” a.target = '_blank'; // force a real navigation on iOS // NOTE: do NOT set a.download here – we want the server's Content-Disposition to drive it document.body.appendChild(a); a.click(); document.body.removeChild(a); } // Global variable to store the current fetch's AbortController. let currentFetchController = null; async function startPlaying(relUrl) { // Pause the audio and clear its source. audio.pause(); audio.src = ''; // Display the audio player container. audioPlayerContainer.style.display = "block"; // Set a timeout to display a loader message if needed. const loaderTimeout = setTimeout(() => { playerButton.innerHTML = playIcon; nowPlayingInfo.textContent = "Wird geladen..."; }, 250); const spinnerTimer = setTimeout(showSpinner, 500); footer.style.display = 'flex'; // Abort any previous fetch if still running. if (currentFetchController) { currentFetchController.abort(); } currentFetchController = new AbortController(); 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. audio.src = mediaUrl; audio.load(); await audio.play(); clearTimeout(spinnerTimer); hideSpinner(); 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: fileName.replace(/\.[^/.]+$/, ''), // remove extension artist: folderName, artwork: [ { src: '/icons/logo-192x192.png', sizes: '192x192', type: 'image/png' } ] }); navigator.mediaSession.playbackState = 'playing'; navigator.mediaSession.setPositionState({ duration: audio.duration, playbackRate: audio.playbackRate, position: 0 }); } nowPlayingInfo.innerHTML = pathStr.replace(/\//g, ' > ') + '
' + fileName.replace('.mp3', '') + ''; } 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."; } }; } if ('mediaSession' in navigator) { // Handler for the play action navigator.mediaSession.setActionHandler('play', () => { navigator.mediaSession.playbackState = 'playing' document.getElementById('globalAudio').play(); }); // Handler for the pause action navigator.mediaSession.setActionHandler('pause', () => { navigator.mediaSession.playbackState = 'paused' 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(); } } }); // Handler for the seek backward action navigator.mediaSession.setActionHandler('seekto', ({seekTime, fastSeek}) => { if (fastSeek && 'fastSeek' in audio) { audio.fastSeek(seekTime); } else { audio.currentTime = seekTime; } // immediately update the remote clock navigator.mediaSession.setPositionState({ duration: audio.duration, playbackRate: audio.playbackRate, position: audio.currentTime }); }); }