(function () { 'use strict'; const audio = document.getElementById('player-audio'); const playerNext = document.getElementById('player-next'); const playerRep = document.getElementById('player-repeat'); const playerLike = document.getElementById('player-like'); // ── Build overlay DOM ────────────────────────────────────────────────────── const overlay = document.createElement('div'); overlay.id = 'np-overlay'; overlay.setAttribute('aria-modal', 'true'); overlay.setAttribute('role', 'dialog'); overlay.setAttribute('aria-label', 'Now Playing'); overlay.innerHTML = '
' + '' + /* Left panel */ '
' + '
' + '
' + '
' + '

Created by

' + '

' + '
' + '
' + /* Right panel */ '
' + '
' + '
' + '
' + '
' + '' + '
' + '
' + '
' + '' + '' + '' + '' + '' + '
' + '
' + '
' + '
'; document.body.appendChild(overlay); // Element refs const modal = overlay.querySelector('#np-modal'); const closeBtn = overlay.querySelector('#np-close'); const coverImg = overlay.querySelector('#np-cover'); const creatorWrap = overlay.querySelector('#np-creator-avatar-wrap'); const creatorName = overlay.querySelector('#np-creator-name'); const plTitle = overlay.querySelector('#np-pl-title'); const plMeta = overlay.querySelector('#np-pl-meta'); const tracksList = overlay.querySelector('#np-tracks'); const progressWrap = overlay.querySelector('#np-progress-wrap'); const progressFill = overlay.querySelector('#np-progress-fill'); const npPlayPauseBtn = overlay.querySelector('#np-btn-playpause'); const npNextBtn = overlay.querySelector('#np-btn-next'); const npRepeatBtn = overlay.querySelector('#np-btn-repeat'); const npLikeBtn = overlay.querySelector('.btn-social[data-social-action="like"]'); const npFavBtn = overlay.querySelector('.btn-social[data-social-action="favorite"]'); // ── State ────────────────────────────────────────────────────────────────── let meta = null; let queue = []; let isOpen = false; let closing = false; let queueFromNpBtn = false; // true when current queue was started from [data-nowplaying-meta] // ── Helpers ──────────────────────────────────────────────────────────────── function esc(str) { return String(str) .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function buildTrackList() { tracksList.innerHTML = ''; queue.forEach((track, i) => { const li = document.createElement('li'); li.className = 'np-track'; li.dataset.trackId = track.id; li.innerHTML = '' + '' + (i + 1) + '' + '' + '
' + esc(track.title || '') + '
' + '
' + esc(track.artist || '') + '
' + '
'; tracksList.appendChild(li); }); } function setCover(src) { if (!src) { coverImg.style.opacity = '0'; return; } if (coverImg.src === src) return; coverImg.style.opacity = '0'; coverImg.onload = () => { coverImg.style.opacity = '1'; }; coverImg.onerror = () => { coverImg.style.opacity = '0'; }; coverImg.src = src; } function setCurrentTrack(song) { // Update cover (fallback to playlist cover if track has none). setCover(song.cover || (meta && meta.cover) || ''); // Highlight active row and scroll it into view. tracksList.querySelectorAll('.np-track').forEach(li => { const active = String(li.dataset.trackId) === String(song.id); li.classList.toggle('np-active', active); if (active) { li.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); } }); // Update bottom-bar social buttons. const hasSong = !!song.id; npLikeBtn.disabled = npFavBtn.disabled = !hasSong; npLikeBtn.dataset.socialId = npFavBtn.dataset.socialId = hasSong ? song.id : ''; npLikeBtn.classList.toggle('active', !!song.userLiked); npLikeBtn.setAttribute('aria-pressed', String(!!song.userLiked)); npFavBtn.classList.toggle('active', !!song.userFavorited); npFavBtn.setAttribute('aria-pressed', String(!!song.userFavorited)); const lc = npLikeBtn.querySelector('.social-count'); const fc = npFavBtn.querySelector('.social-count'); if (lc) lc.textContent = song.likeCount ?? 0; if (fc) fc.textContent = song.favoriteCount ?? 0; } function populateCreator(creatorData) { creatorWrap.innerHTML = ''; if (!creatorData) { overlay.querySelector('#np-creator-info').style.display = 'none'; return; } overlay.querySelector('#np-creator-info').style.display = ''; if (creatorData.avatarPath) { const img = document.createElement('img'); img.src = '/media/' + creatorData.avatarPath; img.alt = ''; img.className = 'np-creator-avatar'; creatorWrap.appendChild(img); } else { const ph = document.createElement('div'); ph.className = 'np-creator-avatar np-creator-placeholder'; ph.textContent = (creatorData.displayName || '?').charAt(0).toUpperCase(); creatorWrap.appendChild(ph); } creatorName.textContent = creatorData.displayName || ''; } function syncPlayPause() { const playing = audio && !audio.paused && !audio.ended; npPlayPauseBtn.textContent = playing ? '\u23f8' : '\u25b6'; npPlayPauseBtn.setAttribute('aria-pressed', String(playing)); npPlayPauseBtn.title = playing ? 'Pause' : 'Play'; } function syncRepeat() { const on = playerRep && playerRep.classList.contains('active'); npRepeatBtn.classList.toggle('active', on); npRepeatBtn.setAttribute('aria-pressed', String(on)); } // If the user is a guest, social buttons should redirect to login. function applyLoginGuard() { const needsLogin = playerLike && playerLike.hasAttribute('data-require-login'); if (needsLogin) { npLikeBtn.setAttribute('data-require-login', ''); npFavBtn.setAttribute('data-require-login', ''); } else { npLikeBtn.removeAttribute('data-require-login'); npFavBtn.removeAttribute('data-require-login'); } } // ── Open / close ─────────────────────────────────────────────────────────── function open(metaData) { if (closing) return; meta = metaData; plTitle.textContent = meta.title || ''; const parts = []; if (meta.trackCount) parts.push(meta.trackCount + ' track' + (meta.trackCount === 1 ? '' : 's')); if (meta.description) parts.push(meta.description); plMeta.textContent = parts.join(' · '); populateCreator(meta.creator || null); applyLoginGuard(); syncRepeat(); syncPlayPause(); // Set initial cover to playlist image; briTunes:play will update it per-track. setCover(meta.cover || ''); // Build track list if we already have the queue (briTunes:queue fires first). if (queue.length) buildTrackList(); // Highlight whichever track is currently active in the player. const cur = window.briTunesNowPlaying; if (cur) setCurrentTrack(cur); overlay.classList.remove('np-closing'); overlay.classList.add('np-open'); document.body.style.overflow = 'hidden'; isOpen = true; } function close() { if (!isOpen || closing) return; closing = true; overlay.classList.add('np-closing'); overlay.addEventListener('animationend', function onEnd() { overlay.removeEventListener('animationend', onEnd); overlay.classList.remove('np-open', 'np-closing'); document.body.style.overflow = ''; isOpen = false; closing = false; }, { once: true }); } // Re-show overlay without re-initialising — used by the "Now Playing" button. function reopen() { if (isOpen || closing || !meta) return; syncRepeat(); const cur = window.briTunesNowPlaying; if (cur) setCurrentTrack(cur); overlay.classList.remove('np-closing'); overlay.classList.add('np-open'); document.body.style.overflow = 'hidden'; isOpen = true; } function setReopenBtn(visible) { const btn = document.getElementById('np-reopen-btn'); if (btn) btn.style.display = visible ? '' : 'none'; } // ── Event wiring ─────────────────────────────────────────────────────────── document.addEventListener('click', (e) => { // Re-open button if (e.target.closest('#np-reopen-btn')) { reopen(); return; } // Play All / Shuffle All on a playlist page const btn = e.target.closest('[data-nowplaying-meta]'); if (btn) { queueFromNpBtn = true; setReopenBtn(true); try { open(JSON.parse(btn.getAttribute('data-nowplaying-meta'))); } catch (err) { console.error('[now-playing]', err); } return; } if (e.target === overlay) close(); }); closeBtn.addEventListener('click', close); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && isOpen) close(); }); // Receive final (post-shuffle) queue from player.js. // Fires synchronously INSIDE the player.js click handler, so queueFromNpBtn // is still false here. The now-playing.js click handler runs next and sets it // true when appropriate, also showing the reopen button. window.addEventListener('briTunes:queue', (e) => { queueFromNpBtn = false; queue = e.detail.queue || []; if (isOpen) buildTrackList(); // Hide the reopen button; the click handler will re-show it if this came // from a [data-nowplaying-meta] button. setReopenBtn(false); }); // Track changes. window.addEventListener('briTunes:play', (e) => { if (!isOpen) return; setCurrentTrack(e.detail); }); // Next / Repeat — delegate to actual player bar buttons. npPlayPauseBtn.addEventListener('click', () => { if (!audio) return; if (audio.paused || audio.ended) audio.play().catch(() => {}); else audio.pause(); }); if (audio) { audio.addEventListener('play', () => { if (isOpen) syncPlayPause(); }); audio.addEventListener('pause', () => { if (isOpen) syncPlayPause(); }); } npNextBtn.addEventListener('click', () => { if (playerNext) playerNext.click(); }); npRepeatBtn.addEventListener('click', () => { if (playerRep) playerRep.click(); // State update will be caught by the listener below. }); if (playerRep) { playerRep.addEventListener('click', () => { // Use a microtask so the class has already been toggled by player.js. Promise.resolve().then(syncRepeat); }); } // Progress bar. if (audio) { audio.addEventListener('timeupdate', () => { if (!isOpen || !audio.duration) return; progressFill.style.width = (audio.currentTime / audio.duration * 100) + '%'; }); } progressWrap.addEventListener('click', (e) => { if (!audio || !audio.duration) return; const rect = progressWrap.getBoundingClientRect(); audio.currentTime = ((e.clientX - rect.left) / rect.width) * audio.duration; }); })();