velora-player/assets/js/player.js
2026-03-23 16:55:03 +01:00

383 lines
10 KiB
JavaScript

( function() {
function formatTime( totalSeconds ) {
if ( ! Number.isFinite( totalSeconds ) || totalSeconds < 0 ) {
return '0:00';
}
const minutes = Math.floor( totalSeconds / 60 );
const seconds = Math.floor( totalSeconds % 60 );
return minutes + ':' + String( seconds ).padStart( 2, '0' );
}
function initPlayers( root ) {
root.querySelectorAll( '[data-map-player]' ).forEach( initPlayer );
}
function initPlayer( root ) {
if ( root.dataset.mapInitialized === 'true' ) {
return;
}
const audio = root.querySelector( '[data-map-audio]' );
const toggle = root.querySelector( '[data-map-toggle]' );
const previousButton = root.querySelector( '[data-map-previous]' );
const nextButton = root.querySelector( '[data-map-next]' );
const repeatButton = root.querySelector( '[data-map-repeat]' );
const progress = root.querySelector( '[data-map-progress]' );
const currentTime = root.querySelector( '[data-map-current]' );
const duration = root.querySelector( '[data-map-duration]' );
const title = root.querySelector( '[data-map-title]' );
const coverWrap = root.querySelector( '.map-player__cover-wrap' );
const cover = root.querySelector( '[data-map-cover]' );
const toggleText = root.querySelector( '.map-player__toggle-text' );
const editorNotice = root.querySelector( '[data-map-editor-notice]' );
const isEditorPreview = root.dataset.mapEditorPreview === 'true';
const showCoverSetting = root.dataset.mapShowCoverSetting === 'true';
const trackedHashes = new Set();
let isRepeatEnabled = root.dataset.mapRepeatEnabled === 'true';
let currentIndex = Number( root.dataset.mapCurrentIndex || 0 );
let playlist = [];
if ( ! audio || ! toggle || ! progress || ! currentTime || ! duration ) {
return;
}
try {
const parsedPlaylist = JSON.parse( root.dataset.mapPlaylist || '[]' );
if ( Array.isArray( parsedPlaylist ) ) {
playlist = parsedPlaylist.filter( function( track ) {
return track && track.src;
} );
}
} catch ( error ) {
playlist = [];
}
if ( ! playlist.length && root.dataset.trackSrc ) {
playlist = [
{
src: root.dataset.trackSrc,
title: root.dataset.trackTitle || 'Untitled Track',
image: cover && cover.getAttribute( 'src' ) ? cover.getAttribute( 'src' ) : '',
hash: root.dataset.trackHash || '',
nonce: root.dataset.trackNonce || ''
}
];
}
if ( ! playlist.length ) {
return;
}
currentIndex = Math.max( 0, Math.min( currentIndex, playlist.length - 1 ) );
root.dataset.mapInitialized = 'true';
function getCurrentTrack() {
return playlist[ currentIndex ] || playlist[ 0 ];
}
function setEditorNotice( message ) {
if ( ! editorNotice ) {
return;
}
editorNotice.textContent = message;
}
function syncTrackDataset( track ) {
root.dataset.mapCurrentIndex = String( currentIndex );
root.dataset.trackSrc = track.src || '';
root.dataset.trackTitle = track.title || '';
root.dataset.trackHash = track.hash || '';
root.dataset.trackNonce = track.nonce || '';
}
function updateCoverUI( track ) {
const shouldShowCover = showCoverSetting && !! track.image;
root.classList.toggle( 'map-player--no-cover', ! shouldShowCover );
root.dataset.mapHasCover = shouldShowCover ? 'true' : 'false';
if ( ! coverWrap || ! cover ) {
return;
}
coverWrap.style.display = shouldShowCover ? '' : 'none';
if ( shouldShowCover ) {
cover.src = track.image;
cover.alt = track.title || 'Audio cover';
}
}
function updateTransportState() {
const atFirstTrack = currentIndex === 0;
const atLastTrack = currentIndex === playlist.length - 1;
if ( previousButton ) {
previousButton.disabled = atFirstTrack;
}
if ( nextButton ) {
nextButton.disabled = atLastTrack;
}
if ( repeatButton ) {
repeatButton.classList.toggle( 'is-active', isRepeatEnabled );
repeatButton.setAttribute( 'aria-pressed', isRepeatEnabled ? 'true' : 'false' );
}
}
function updateProgressUI() {
if ( ! Number.isFinite( audio.duration ) || audio.duration <= 0 ) {
progress.value = 0;
duration.textContent = '0:00';
currentTime.textContent = '0:00';
progress.style.setProperty( '--map-progress-percent', '0%' );
return;
}
const percent = ( audio.currentTime / audio.duration ) * 100;
progress.value = percent;
progress.style.setProperty( '--map-progress-percent', percent + '%' );
currentTime.textContent = formatTime( audio.currentTime );
duration.textContent = formatTime( audio.duration );
}
function setPlayingState( isPlaying ) {
root.classList.toggle( 'is-playing', isPlaying );
toggle.setAttribute( 'aria-pressed', isPlaying ? 'true' : 'false' );
toggle.setAttribute( 'aria-label', isPlaying ? 'Pause audio' : 'Play audio' );
toggleText.textContent = isPlaying ? 'Pause' : 'Play';
}
function applyTrack( options ) {
const track = getCurrentTrack();
const autoplay = !! ( options && options.autoplay );
if ( title ) {
title.textContent = track.title || 'Untitled Track';
}
updateCoverUI( track );
updateTransportState();
syncTrackDataset( track );
if ( audio.getAttribute( 'src' ) !== track.src ) {
audio.setAttribute( 'src', track.src );
}
if ( audio.src !== track.src ) {
audio.src = track.src;
}
audio.currentTime = 0;
audio.load();
updateProgressUI();
if ( autoplay ) {
audio.play().catch( function() {
if ( isEditorPreview ) {
setEditorNotice( 'Playback preview is limited in the editor. Test full playback on the frontend.' );
}
} );
}
}
function ensureAudioLoaded() {
const track = getCurrentTrack();
if ( ! track || ! track.src ) {
return;
}
if ( audio.getAttribute( 'src' ) !== track.src ) {
applyTrack( { autoplay: false } );
return;
}
if ( audio.networkState === HTMLMediaElement.NETWORK_EMPTY || ! Number.isFinite( audio.duration ) || audio.duration <= 0 ) {
try {
audio.load();
} catch ( error ) {
return;
}
}
}
function trackPlay() {
const track = getCurrentTrack();
if ( ! track || ! track.src || ! track.hash || ! track.nonce || ! root.dataset.restEndpoint || trackedHashes.has( track.hash ) ) {
return;
}
trackedHashes.add( track.hash );
window.fetch( root.dataset.restEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify( {
src: track.src,
title: track.title || '',
hash: track.hash,
nonce: track.nonce
} ),
credentials: 'same-origin'
} ).catch( function() {
return null;
} );
}
function switchTrack( nextIndex, options ) {
if ( nextIndex < 0 || nextIndex >= playlist.length || nextIndex === currentIndex ) {
return;
}
currentIndex = nextIndex;
applyTrack( options );
}
toggle.addEventListener( 'click', function() {
ensureAudioLoaded();
if ( audio.paused ) {
audio.play().catch( function() {
if ( isEditorPreview ) {
setEditorNotice( 'Playback preview is limited in the editor. Test full playback on the frontend.' );
}
} );
return;
}
audio.pause();
} );
if ( previousButton ) {
previousButton.addEventListener( 'click', function() {
if ( previousButton.disabled ) {
return;
}
if ( audio.currentTime > 5 ) {
audio.currentTime = 0;
updateProgressUI();
return;
}
switchTrack( currentIndex - 1, { autoplay: ! audio.paused } );
} );
}
if ( nextButton ) {
nextButton.addEventListener( 'click', function() {
if ( nextButton.disabled ) {
return;
}
switchTrack( currentIndex + 1, { autoplay: ! audio.paused } );
} );
}
if ( repeatButton ) {
repeatButton.addEventListener( 'click', function() {
isRepeatEnabled = ! isRepeatEnabled;
root.dataset.mapRepeatEnabled = isRepeatEnabled ? 'true' : 'false';
updateTransportState();
} );
}
audio.addEventListener( 'play', function() {
setPlayingState( true );
trackPlay();
} );
audio.addEventListener( 'pause', function() {
setPlayingState( false );
} );
audio.addEventListener( 'loadedmetadata', updateProgressUI );
audio.addEventListener( 'durationchange', updateProgressUI );
audio.addEventListener( 'canplay', updateProgressUI );
audio.addEventListener( 'timeupdate', updateProgressUI );
audio.addEventListener( 'error', function() {
updateProgressUI();
setEditorNotice( 'Unable to load audio preview. Confirm the URL points directly to an audio file.' );
} );
audio.addEventListener( 'ended', function() {
if ( isRepeatEnabled ) {
audio.currentTime = 0;
audio.play().catch( function() {
return null;
} );
return;
}
if ( currentIndex < playlist.length - 1 ) {
switchTrack( currentIndex + 1, { autoplay: true } );
return;
}
setPlayingState( false );
audio.currentTime = 0;
updateProgressUI();
} );
progress.addEventListener( 'input', function() {
if ( ! Number.isFinite( audio.duration ) || audio.duration <= 0 ) {
return;
}
const targetTime = ( Number( progress.value ) / 100 ) * audio.duration;
audio.currentTime = targetTime;
updateProgressUI();
} );
applyTrack( { autoplay: false } );
}
function observePlayerMounts() {
if ( ! document.body || typeof MutationObserver === 'undefined' ) {
return;
}
const observer = new MutationObserver( function( mutations ) {
mutations.forEach( function( mutation ) {
mutation.addedNodes.forEach( function( node ) {
if ( ! node || node.nodeType !== 1 ) {
return;
}
if ( node.matches && node.matches( '[data-map-player]' ) ) {
initPlayer( node );
return;
}
if ( node.querySelectorAll ) {
initPlayers( node );
}
} );
} );
} );
observer.observe( document.body, {
childList: true,
subtree: true
} );
}
function boot() {
initPlayers( document );
observePlayerMounts();
}
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', boot );
} else {
boot();
}
} )();