// 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
});
});
}