rework audioplayer
This commit is contained in:
parent
880fe9e97c
commit
dbebdf79fd
@ -45,9 +45,6 @@ function paintFile() {
|
|||||||
if (currentMusicFileElement) {
|
if (currentMusicFileElement) {
|
||||||
const fileItem = currentMusicFileElement.closest('.file-item');
|
const fileItem = currentMusicFileElement.closest('.file-item');
|
||||||
fileItem.classList.add('currently-playing');
|
fileItem.classList.add('currently-playing');
|
||||||
// setTimeout(() => {
|
|
||||||
// fileItem.scrollIntoView({ block: "center", inline: "nearest" });
|
|
||||||
// }, 300);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -310,7 +307,7 @@ document.querySelectorAll('.play-file').forEach(link => {
|
|||||||
// Mark the clicked item as currently playing.
|
// Mark the clicked item as currently playing.
|
||||||
this.closest('.file-item').classList.add('currently-playing');
|
this.closest('.file-item').classList.add('currently-playing');
|
||||||
|
|
||||||
startPlaying(relUrl);
|
player.loadTrack(relUrl);
|
||||||
|
|
||||||
// Delay preloading to avoid blocking playback.
|
// Delay preloading to avoid blocking playback.
|
||||||
setTimeout(preload_audio, 1000);
|
setTimeout(preload_audio, 1000);
|
||||||
|
|||||||
@ -1,257 +1,207 @@
|
|||||||
// read the CSS variable from :root (or any selector)
|
class SimpleAudioPlayer {
|
||||||
const cssVar = getComputedStyle(document.documentElement).getPropertyValue('--dark-background').trim();
|
constructor({
|
||||||
|
audioSelector = '#globalAudio',
|
||||||
|
playBtnSelector = '.player-button',
|
||||||
|
timelineSelector = '.timeline',
|
||||||
|
infoSelector = '#timeInfo',
|
||||||
|
nowPlayingSelector = '#nowPlayingInfo',
|
||||||
|
containerSelector = '#audioPlayerContainer',
|
||||||
|
footerSelector = 'footer'
|
||||||
|
} = {}) {
|
||||||
|
// Elements
|
||||||
|
this.audio = document.querySelector(audioSelector);
|
||||||
|
this.playBtn = document.querySelector(playBtnSelector);
|
||||||
|
this.timeline = document.querySelector(timelineSelector);
|
||||||
|
this.timeInfo = document.querySelector(infoSelector);
|
||||||
|
this.nowInfo = document.querySelector(nowPlayingSelector);
|
||||||
|
this.container = document.querySelector(containerSelector);
|
||||||
|
this.footer = document.querySelector(footerSelector);
|
||||||
|
|
||||||
// player DOM elements
|
// State
|
||||||
const nowPlayingInfo = document.getElementById('nowPlayingInfo');
|
this.isSeeking = false;
|
||||||
const audio = document.getElementById('globalAudio');
|
this.rafId = null;
|
||||||
const audioPlayerContainer = document.getElementById('audioPlayerContainer');
|
this.abortCtrl = null;
|
||||||
|
this._posInterval = null;
|
||||||
|
|
||||||
const playerButton = document.querySelector('.player-button'),
|
// Pre-compute icons once
|
||||||
timeline = document.querySelector('.timeline'),
|
const fill = getComputedStyle(document.documentElement)
|
||||||
timeInfo = document.getElementById('timeInfo'),
|
.getPropertyValue('--dark-background').trim();
|
||||||
playIcon = `
|
this.icons = {
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="${cssVar}">
|
play : this.svg(
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
|
'M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z',
|
||||||
</svg>
|
fill
|
||||||
`,
|
),
|
||||||
pauseIcon = `
|
pause: this.svg(
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="${cssVar}">
|
'M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z',
|
||||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
fill
|
||||||
</svg>
|
),
|
||||||
`,
|
sound: this.svg(
|
||||||
soundIcon = `
|
'M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM14.657 2.929a1 1 0 011.414 0A9.972 9.972 0 0119 10a9.972 9.972 0 01-2.929 7.071 1 1 0 01-1.414-1.414A7.971 7.971 0 0017 10c0-2.21-.894-4.208-2.343-5.657a1 1 0 010-1.414zm-2.829 2.828a1 1 0 011.415 0A5.983 5.983 0 0115 10a5.984 5.984 0 01-1.757 4.243 1 1 0 01-1.415-1.415A3.984 3.984 0 0013 10a3.983 3.983 0 00-1.172-2.828 1 1 0 010-1.415z',
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="${cssVar}">
|
fill
|
||||||
<path fill-rule="evenodd" d="M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM14.657 2.929a1 1 0 011.414 0A9.972 9.972 0 0119 10a9.972 9.972 0 01-2.929 7.071 1 1 0 01-1.414-1.414A7.971 7.971 0 0017 10c0-2.21-.894-4.208-2.343-5.657a1 1 0 010-1.414zm-2.829 2.828a1 1 0 011.415 0A5.983 5.983 0 0115 10a5.984 5.984 0 01-1.757 4.243 1 1 0 01-1.415-1.415A3.984 3.984 0 0013 10a3.983 3.983 0 00-1.172-2.828 1 1 0 010-1.415z" clip-rule="evenodd" />
|
)
|
||||||
</svg>
|
};
|
||||||
`
|
|
||||||
|
|
||||||
function toggleAudio () {
|
// Seeked => refresh timeline if external seek (e.g. Android controls)
|
||||||
if (audio.paused) {
|
this.audio.addEventListener('seeked', () => this.updateTimeline());
|
||||||
audio.play();
|
|
||||||
playerButton.innerHTML = pauseIcon;
|
// Bind UI events
|
||||||
} else {
|
this._bindEvents();
|
||||||
audio.pause();
|
|
||||||
playerButton.innerHTML = playIcon;
|
// Setup MediaSession handlers + heartbeat
|
||||||
|
this._initMediaSession();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
playerButton.addEventListener('click', toggleAudio);
|
|
||||||
|
|
||||||
let isSeeking = false;
|
// Minimal SVG helper
|
||||||
|
svg(pathD, fill) {
|
||||||
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="${fill}">
|
||||||
|
<path fill-rule="evenodd" d="${pathD}" clip-rule="evenodd"/>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Slider (Timeline) events ---
|
_bindEvents() {
|
||||||
// Mouse events
|
this.playBtn.addEventListener('click', () => this.togglePlay());
|
||||||
timeline.addEventListener('mousedown', () => { isSeeking = true; });
|
|
||||||
timeline.addEventListener('mouseup', () => { isSeeking = false;
|
|
||||||
changeSeek(); // Update the audio currentTime based on the slider position
|
|
||||||
});
|
|
||||||
|
|
||||||
// Touch events
|
this.audio.addEventListener('loadedmetadata', () => {
|
||||||
timeline.addEventListener('touchstart', () => { isSeeking = true; });
|
this.timeline.min = 0;
|
||||||
timeline.addEventListener('touchend', () => { isSeeking = false;
|
this.timeline.max = this.audio.duration;
|
||||||
changeSeek();
|
this.timeline.value = 0;
|
||||||
});
|
this.timeline.style.backgroundSize = '0% 100%';
|
||||||
timeline.addEventListener('touchcancel', () => { isSeeking = false; });
|
});
|
||||||
|
|
||||||
|
this.audio.addEventListener('play', () => this._updatePlayState());
|
||||||
|
this.audio.addEventListener('pause', () => this._updatePlayState());
|
||||||
|
this.audio.addEventListener('ended', () => this.playBtn.innerHTML = this.icons.play);
|
||||||
|
|
||||||
// --- Seek function: directly set audio.currentTime using slider's value (in seconds) ---
|
// Native timeupdate => update timeline
|
||||||
function changeSeek() {
|
this.audio.addEventListener('timeupdate', () => this.updateTimeline());
|
||||||
audio.currentTime = timeline.value;
|
|
||||||
|
|
||||||
if ('mediaSession' in navigator && 'setPositionState' in navigator.mediaSession) {
|
// Fallback interval for throttled mobile
|
||||||
|
this._interval = setInterval(() => {
|
||||||
|
if (!this.audio.paused && !this.isSeeking) {
|
||||||
|
this.updateTimeline();
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// Unified seek input
|
||||||
|
this.timeline.addEventListener('input', () => {
|
||||||
|
this.isSeeking = true;
|
||||||
|
this.audio.currentTime = this.timeline.value;
|
||||||
|
|
||||||
|
// immediate Android sync
|
||||||
|
if (navigator.mediaSession?.setPositionState) {
|
||||||
navigator.mediaSession.setPositionState({
|
navigator.mediaSession.setPositionState({
|
||||||
duration: audio.duration,
|
duration: this.audio.duration,
|
||||||
playbackRate: audio.playbackRate,
|
playbackRate: this.audio.playbackRate,
|
||||||
position: audio.currentTime
|
position: this.audio.currentTime
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// --- Utility: Format seconds as mm:ss ---
|
this.updateTimeline();
|
||||||
function formatTime(seconds) {
|
this.isSeeking = false;
|
||||||
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)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 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%';
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Update timeline and time info on time update ---
|
|
||||||
audio.addEventListener('timeupdate', () => {
|
|
||||||
updateTimeline();
|
|
||||||
|
|
||||||
if ('mediaSession' in navigator && 'setPositionState' in navigator.mediaSession) {
|
|
||||||
navigator.mediaSession.setPositionState({
|
|
||||||
duration: audio.duration,
|
|
||||||
playbackRate: audio.playbackRate,
|
|
||||||
position: audio.currentTime
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// --- Update media session metadata when audio is loaded ---
|
togglePlay() {
|
||||||
audio.addEventListener('play', () => {
|
this.audio.paused ? this.audio.play() : this.audio.pause();
|
||||||
if ('mediaSession' in navigator)
|
|
||||||
navigator.mediaSession.playbackState = 'playing';
|
|
||||||
});
|
|
||||||
audio.addEventListener('pause', () => {
|
|
||||||
if ('mediaSession' in navigator)
|
|
||||||
navigator.mediaSession.playbackState = 'paused';
|
|
||||||
});
|
|
||||||
|
|
||||||
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}`;
|
_updatePlayState() {
|
||||||
|
this.playBtn.innerHTML = this.audio.paused ? this.icons.play : this.icons.pause;
|
||||||
|
if ('mediaSession' in navigator) {
|
||||||
|
navigator.mediaSession.playbackState = this.audio.paused ? 'paused' : 'playing';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_formatTime(sec) {
|
||||||
|
const m = Math.floor(sec / 60), s = Math.floor(sec % 60);
|
||||||
|
return `${m < 10 ? '0' : ''}${m}:${s < 10 ? '0' : ''}${s}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTimeline() {
|
||||||
|
if (!this.audio.duration || this.isSeeking) return;
|
||||||
|
|
||||||
|
// 1) Move the thumb
|
||||||
|
this.timeline.value = this.audio.currentTime;
|
||||||
|
// 2) Resize fill
|
||||||
|
const pct = (this.audio.currentTime / this.audio.duration) * 100;
|
||||||
|
this.timeline.style.backgroundSize = `${pct}% 100%`;
|
||||||
|
// 3) Update text
|
||||||
|
this.timeInfo.textContent =
|
||||||
|
`${this._formatTime(this.audio.currentTime)} / ${this._formatTime(this.audio.duration)}`;
|
||||||
|
// 4) Push to Android widget
|
||||||
|
if (navigator.mediaSession?.setPositionState) {
|
||||||
|
navigator.mediaSession.setPositionState({
|
||||||
|
duration: this.audio.duration,
|
||||||
|
playbackRate: this.audio.playbackRate,
|
||||||
|
position: this.audio.currentTime
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadTrack(relUrl) {
|
||||||
|
this.audio.pause();
|
||||||
|
this.container.style.display = 'block';
|
||||||
|
this.nowInfo.textContent = 'Loading…';
|
||||||
|
|
||||||
|
this.abortCtrl?.abort();
|
||||||
|
this.abortCtrl = new AbortController();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Perform a HEAD request to verify media availability.
|
const head = await fetch(`/media/${relUrl}`, {
|
||||||
const response = await fetch(mediaUrl, { method: 'HEAD', signal: currentFetchController.signal });
|
method: 'HEAD',
|
||||||
clearTimeout(loaderTimeout);
|
signal: this.abortCtrl.signal
|
||||||
|
});
|
||||||
|
if (!head.ok) throw new Error(`Status ${head.status}`);
|
||||||
|
|
||||||
if (response.status === 403) {
|
this.audio.src = `/media/${relUrl}`;
|
||||||
nowPlayingInfo.textContent = "Fehler: Zugriff verweigert.";
|
await this.audio.play();
|
||||||
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.
|
// Full breadcrumb
|
||||||
audio.src = mediaUrl;
|
const parts = relUrl.split('/');
|
||||||
audio.load();
|
const file = parts.pop();
|
||||||
await audio.play();
|
const folderPath = parts.join(' › ');
|
||||||
clearTimeout(spinnerTimer);
|
this.nowInfo.innerHTML =
|
||||||
hideSpinner();
|
`${folderPath} › <strong>${file.replace(/\.[^/.]+$/, '')}</strong>`;
|
||||||
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) {
|
if ('mediaSession' in navigator) {
|
||||||
navigator.mediaSession.metadata = new MediaMetadata({
|
navigator.mediaSession.metadata = new MediaMetadata({
|
||||||
title: fileName.replace(/\.[^/.]+$/, ''), // remove extension
|
title : file.replace(/\.[^/.]+$/, ''),
|
||||||
artist: folderName,
|
artist: parts.pop(),
|
||||||
artwork: [
|
artwork: [{ src:'/icons/logo-192x192.png', sizes:'192x192', type:'image/png' }]
|
||||||
{ 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
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err.name !== 'AbortError') {
|
||||||
|
this.nowInfo.textContent = 'Error loading track';
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
nowPlayingInfo.innerHTML = pathStr.replace(/\//g, ' > ') +
|
_initMediaSession() {
|
||||||
'<br><span style="font-size: larger; font-weight: bold;">' +
|
if (!('mediaSession' in navigator)) return;
|
||||||
fileName.replace('.mp3', '') + '</span>';
|
|
||||||
} catch (error) {
|
const wrap = fn => details => {
|
||||||
if (error.name === 'AbortError') {
|
fn(details);
|
||||||
console.log('Previous fetch aborted.');
|
this.updateTimeline();
|
||||||
} else {
|
|
||||||
console.error('Error fetching media:', error);
|
|
||||||
nowPlayingInfo.textContent = "Fehler: Netzwerkproblem oder ungültige URL.";
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
// Core controls
|
||||||
|
navigator.mediaSession.setActionHandler('play', wrap(() => this.audio.play()));
|
||||||
|
navigator.mediaSession.setActionHandler('pause', wrap(() => this.audio.pause()));
|
||||||
|
navigator.mediaSession.setActionHandler('seekbackward', wrap(({seekOffset=10}) => {
|
||||||
|
this.audio.currentTime = Math.max(0, this.audio.currentTime - seekOffset);
|
||||||
|
}));
|
||||||
|
navigator.mediaSession.setActionHandler('seekforward', wrap(({seekOffset=10}) => {
|
||||||
|
this.audio.currentTime = Math.min(this.audio.duration, this.audio.currentTime + seekOffset);
|
||||||
|
}));
|
||||||
|
navigator.mediaSession.setActionHandler('seekto', wrap(({seekTime, fastSeek}) => {
|
||||||
|
if (fastSeek && this.audio.fastSeek) this.audio.fastSeek(seekTime);
|
||||||
|
else this.audio.currentTime = seekTime;
|
||||||
|
}));
|
||||||
|
|
||||||
if ('mediaSession' in navigator) {
|
// Prev & Next
|
||||||
|
navigator.mediaSession.setActionHandler('previoustrack', wrap(() => {
|
||||||
// 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) {
|
if (currentMusicIndex > 0) {
|
||||||
const prevFile = currentMusicFiles[currentMusicIndex - 1];
|
const prevFile = currentMusicFiles[currentMusicIndex - 1];
|
||||||
const prevLink = document.querySelector(`.play-file[data-url="${prevFile.path}"]`);
|
const prevLink = document.querySelector(`.play-file[data-url="${prevFile.path}"]`);
|
||||||
@ -259,42 +209,31 @@ if ('mediaSession' in navigator) {
|
|||||||
prevLink.click();
|
prevLink.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
navigator.mediaSession.setActionHandler('nexttrack', wrap(() => {
|
||||||
// Handler for the next track action
|
if (currentMusicIndex < currentMusicFiles.length - 1) {
|
||||||
navigator.mediaSession.setActionHandler('nexttrack', () => {
|
|
||||||
if (currentMusicIndex >= 0 && currentMusicIndex < currentMusicFiles.length - 1) {
|
|
||||||
const nextFile = currentMusicFiles[currentMusicIndex + 1];
|
const nextFile = currentMusicFiles[currentMusicIndex + 1];
|
||||||
const nextLink = document.querySelector(`.play-file[data-url="${nextFile.path}"]`);
|
const nextLink = document.querySelector(`.play-file[data-url="${nextFile.path}"]`);
|
||||||
if (nextLink) {
|
if (nextLink) {
|
||||||
nextLink.click();
|
nextLink.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
// Handler for the seek backward and seek forward actions
|
// Heartbeat for widget
|
||||||
navigator.mediaSession.setActionHandler('seekbackward', details => {
|
this._posInterval = setInterval(() => {
|
||||||
const offset = details.seekOffset || 10;
|
if (!this.audio.paused && navigator.mediaSession.setPositionState) {
|
||||||
audio.currentTime = Math.max(0, audio.currentTime - offset);
|
|
||||||
});
|
|
||||||
navigator.mediaSession.setActionHandler('seekforward', details => {
|
|
||||||
const offset = details.seekOffset || 10;
|
|
||||||
audio.currentTime = Math.min(audio.duration, audio.currentTime + offset);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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({
|
navigator.mediaSession.setPositionState({
|
||||||
duration: audio.duration,
|
duration: this.audio.duration,
|
||||||
playbackRate: audio.playbackRate,
|
playbackRate: this.audio.playbackRate,
|
||||||
position: audio.currentTime
|
position: this.audio.currentTime
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize instance
|
||||||
|
const player = new SimpleAudioPlayer();
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
300
static/audioplayer_bak.js
Normal file
300
static/audioplayer_bak.js
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
// 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 = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="${cssVar}">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
pauseIcon = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="${cssVar}">
|
||||||
|
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
soundIcon = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="${cssVar}">
|
||||||
|
<path fill-rule="evenodd" d="M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM14.657 2.929a1 1 0 011.414 0A9.972 9.972 0 0119 10a9.972 9.972 0 01-2.929 7.071 1 1 0 01-1.414-1.414A7.971 7.971 0 0017 10c0-2.21-.894-4.208-2.343-5.657a1 1 0 010-1.414zm-2.829 2.828a1 1 0 011.415 0A5.983 5.983 0 0115 10a5.984 5.984 0 01-1.757 4.243 1 1 0 01-1.415-1.415A3.984 3.984 0 0013 10a3.983 3.983 0 00-1.172-2.828 1 1 0 010-1.415z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
|
||||||
|
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; });
|
||||||
|
|
||||||
|
|
||||||
|
// --- Seek function: directly set audio.currentTime using slider's value (in seconds) ---
|
||||||
|
function changeSeek() {
|
||||||
|
audio.currentTime = timeline.value;
|
||||||
|
|
||||||
|
if ('mediaSession' in navigator && 'setPositionState' in navigator.mediaSession) {
|
||||||
|
navigator.mediaSession.setPositionState({
|
||||||
|
duration: audio.duration,
|
||||||
|
playbackRate: audio.playbackRate,
|
||||||
|
position: audio.currentTime
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 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)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 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%';
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Update timeline and time info on time update ---
|
||||||
|
audio.addEventListener('timeupdate', () => {
|
||||||
|
updateTimeline();
|
||||||
|
|
||||||
|
if ('mediaSession' in navigator && 'setPositionState' in navigator.mediaSession) {
|
||||||
|
navigator.mediaSession.setPositionState({
|
||||||
|
duration: audio.duration,
|
||||||
|
playbackRate: audio.playbackRate,
|
||||||
|
position: audio.currentTime
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Update media session metadata when audio is loaded ---
|
||||||
|
audio.addEventListener('play', () => {
|
||||||
|
if ('mediaSession' in navigator)
|
||||||
|
navigator.mediaSession.playbackState = 'playing';
|
||||||
|
});
|
||||||
|
audio.addEventListener('pause', () => {
|
||||||
|
if ('mediaSession' in navigator)
|
||||||
|
navigator.mediaSession.playbackState = 'paused';
|
||||||
|
});
|
||||||
|
|
||||||
|
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, ' > ') +
|
||||||
|
'<br><span style="font-size: larger; font-weight: bold;">' +
|
||||||
|
fileName.replace('.mp3', '') + '</span>';
|
||||||
|
} 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 and seek forward actions
|
||||||
|
navigator.mediaSession.setActionHandler('seekbackward', details => {
|
||||||
|
const offset = details.seekOffset || 10;
|
||||||
|
audio.currentTime = Math.max(0, audio.currentTime - offset);
|
||||||
|
});
|
||||||
|
navigator.mediaSession.setActionHandler('seekforward', details => {
|
||||||
|
const offset = details.seekOffset || 10;
|
||||||
|
audio.currentTime = Math.min(audio.duration, audio.currentTime + offset);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
@ -14,7 +14,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
card.className = 'card';
|
card.className = 'card';
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p><button class="btn btn-light" onclick="startPlaying('${file.relative_path}')" style="width:100%;">🔊 ${filenameWithoutExtension}</button></p>
|
<p><button class="btn btn-light" onclick="player.loadTrack('${file.relative_path}')" style="width:100%;">🔊 ${filenameWithoutExtension}</button></p>
|
||||||
<p><button onclick="window.open('/path/${file.relative_path}', '_self');" class="btn btn-light btn-sm" style="width:100%;">📁 ${parentFolder}</button></p>
|
<p><button onclick="window.open('/path/${file.relative_path}', '_self');" class="btn btn-light btn-sm" style="width:100%;">📁 ${parentFolder}</button></p>
|
||||||
<p class="card-text">Anzahl Downloads: ${file.hitcount}</p>
|
<p class="card-text">Anzahl Downloads: ${file.hitcount}</p>
|
||||||
${ file.performance_date !== undefined ? `<p class="card-text">Datum: ${file.performance_date}</p>` : ``}
|
${ file.performance_date !== undefined ? `<p class="card-text">Datum: ${file.performance_date}</p>` : ``}
|
||||||
|
|||||||
@ -245,10 +245,10 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
<!-- Load main app JS first, then gallery JS -->
|
<!-- Load main app JS first, then gallery JS -->
|
||||||
|
<script src="{{ url_for('static', filename='audioplayer.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='app.js') }}"></script>
|
<script src="{{ url_for('static', filename='app.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='gallery.js') }}"></script>
|
<script src="{{ url_for('static', filename='gallery.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='search.js') }}"></script>
|
<script src="{{ url_for('static', filename='search.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='audioplayer.js') }}"></script>
|
|
||||||
<script>
|
<script>
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user