Files
2026-05-11 21:57:24 +02:00

232 lines
7.1 KiB
JavaScript

/**
* Piperless Audio Player
*
* Lightweight custom HTML5 audio player with:
* - Play / Pause toggle
* - Clickable progress bar with seeking
* - Current time display
* - Duration display
* - Buffered progress indication
*
* @package Piperless
*/
( function () {
'use strict';
/**
* Format seconds as mm:ss.
*
* @param {number} secs Duration in seconds.
* @return {string}
*/
function formatTime( secs ) {
if ( isNaN( secs ) || ! isFinite( secs ) ) {
return '00:00';
}
const m = Math.floor( secs / 60 );
const s = Math.floor( secs % 60 );
return String( m ).padStart( 2, '0' ) + ':' + String( s ).padStart( 2, '0' );
}
/**
* Initialise all piperless players on the page.
*/
function initPlayers() {
const containers = document.querySelectorAll( '.piperless-player' );
containers.forEach( function ( container ) {
if ( container.dataset.piperlessInitialised ) {
return;
}
container.dataset.piperlessInitialised = '1';
const audio = container.querySelector( '.piperless-player__audio' );
const playBtn = container.querySelector( '.piperless-player__play' );
const progress = container.querySelector( '.piperless-player__progress' );
const bar = container.querySelector( '.piperless-player__progress-bar' );
const buffered = container.querySelector( '.piperless-player__progress-buffered' );
const current = container.querySelector( '.piperless-player__current' );
const duration = container.querySelector( '.piperless-player__duration' );
const volBtn = container.querySelector( '.piperless-player__volume-btn' );
const volSlider = container.querySelector( '.piperless-player__volume-slider input' );
if ( ! audio || ! playBtn ) {
return;
}
/** @type {boolean} */
let seeking = false;
// ── Play / Pause ─────────────────────────────────────────────
playBtn.addEventListener( 'click', function () {
if ( audio.paused ) {
audio.play().catch( function () {} );
} else {
audio.pause();
}
} );
audio.addEventListener( 'play', function () {
container.classList.add( 'piperless-player--playing' );
} );
audio.addEventListener( 'pause', function () {
container.classList.remove( 'piperless-player--playing' );
} );
// ── Progress bar & seeking ───────────────────────────────────
if ( progress ) {
progress.addEventListener( 'mousedown', function ( e ) {
seeking = true;
seek( e );
} );
document.addEventListener( 'mousemove', function ( e ) {
if ( seeking ) {
seek( e );
}
} );
document.addEventListener( 'mouseup', function () {
seeking = false;
} );
// Touch support.
progress.addEventListener( 'touchstart', function ( e ) {
seeking = true;
seek( e.touches[ 0 ] );
} );
document.addEventListener( 'touchmove', function ( e ) {
if ( seeking ) {
seek( e.touches[ 0 ] );
}
} );
document.addEventListener( 'touchend', function () {
seeking = false;
} );
}
function seek( e ) {
const rect = progress.getBoundingClientRect();
let pct = ( e.clientX - rect.left ) / rect.width;
pct = Math.max( 0, Math.min( 1, pct ) );
if ( audio.duration && isFinite( audio.duration ) ) {
audio.currentTime = pct * audio.duration;
}
if ( bar ) {
bar.style.width = ( pct * 100 ) + '%';
}
}
// ── Time update ──────────────────────────────────────────────
audio.addEventListener( 'timeupdate', function () {
if ( seeking || ! isFinite( audio.duration ) ) {
return;
}
const pct = ( audio.currentTime / audio.duration ) * 100;
if ( bar ) {
bar.style.width = pct + '%';
}
if ( current ) {
current.textContent = formatTime( audio.currentTime );
}
} );
// ── Buffer progress ──────────────────────────────────────────
audio.addEventListener( 'progress', function () {
if ( ! buffered || audio.buffered.length === 0 ) {
return;
}
const bufEnd = audio.buffered.end( audio.buffered.length - 1 );
if ( audio.duration && isFinite( audio.duration ) ) {
const pct = ( bufEnd / audio.duration ) * 100;
buffered.style.width = pct + '%';
}
} );
// ── Metadata loaded ──────────────────────────────────────────
audio.addEventListener( 'loadedmetadata', function () {
if ( current ) {
current.textContent = '00:00';
}
// Duration comes from meta attribute if available.
if ( duration && audio.duration && isFinite( audio.duration ) ) {
duration.textContent = formatTime( audio.duration );
}
} );
// ── End ──────────────────────────────────────────────────────
audio.addEventListener( 'ended', function () {
container.classList.remove( 'piperless-player--playing' );
if ( bar ) {
bar.style.width = '0%';
}
if ( current ) {
current.textContent = '00:00';
}
} );
// ── Error ────────────────────────────────────────────────────
audio.addEventListener( 'error', function () {
container.classList.add( 'piperless-player--error' );
} );
// ── Volume ───────────────────────────────────────────────────
if ( volBtn && volSlider ) {
volBtn.addEventListener( 'click', function ( e ) {
e.stopPropagation();
container.classList.toggle( 'piperless-player--volume-open' );
} );
// Close volume on outside click.
document.addEventListener( 'click', function ( e ) {
if ( ! container.contains( e.target ) ) {
container.classList.remove( 'piperless-player--volume-open' );
}
} );
volSlider.style.setProperty( '--vol-pct', volSlider.value + '%' );
volSlider.addEventListener( 'input', function () {
audio.volume = volSlider.value / 100;
volSlider.style.setProperty( '--vol-pct', volSlider.value + '%' );
} );
}
} );
}
// Initialise on DOM ready.
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', initPlayers );
} else {
initPlayers();
}
// Re-scan if content is dynamically loaded (e.g. infinite scroll).
if ( window.MutationObserver ) {
const observer = new MutationObserver( function ( mutations ) {
let added = false;
for ( let i = 0; i < mutations.length; i++ ) {
if ( mutations[ i ].addedNodes.length ) {
added = true;
break;
}
}
if ( added ) {
initPlayers();
}
} );
observer.observe( document.body, { childList: true, subtree: true } );
}
} )();