(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 */ '
' + /* Playlist mode view */ '
' + '
' + '
' + '
' + '
' + '
    ' + '
    ' + /* Single-song / big-room view */ '
    ' + '

    ' + '

    ' + '

    ' + '' + '
    ' + /* Shared controls bar */ '
    ' + '
    ' + '
    ' + '' + '' + '' + '' + '' + '
    ' + '
    ' + '
    ' + '
    '; 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 playlistView = overlay.querySelector('#np-playlist-view'); const bigroomView = overlay.querySelector('#np-bigroom-view'); const bigroomTitle = overlay.querySelector('#np-bigroom-title'); const bigroomArtist = overlay.querySelector('#np-bigroom-artist'); const bigroomMeta = overlay.querySelector('#np-bigroom-meta'); const bigroomCanvas = overlay.querySelector('#np-viz-big'); 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 singleMode = false; let queueFromNpBtn = false; // ── Big-room visualizer ──────────────────────────────────────────────────── let bigVizRaf = null; let bigFreqData = null; function startBigViz() { if (bigVizRaf) return; const analyser = window.briTunesAnalyser; if (!analyser) { // Audio not yet hooked — retry when it becomes available bigVizRaf = requestAnimationFrame(startBigViz); return; } bigFreqData = bigFreqData || new Uint8Array(analyser.frequencyBinCount); const ctx = bigroomCanvas.getContext('2d'); let lastW = 0, lastH = 0; function frame() { bigVizRaf = requestAnimationFrame(frame); const an = window.briTunesAnalyser; if (!an || !bigFreqData) return; an.getByteFrequencyData(bigFreqData); const w = bigroomCanvas.clientWidth; const h = bigroomCanvas.clientHeight; if (w < 1 || h < 1) return; const dpr = Math.min(window.devicePixelRatio || 1, 2); if (w !== lastW || h !== lastH) { bigroomCanvas.width = Math.round(w * dpr); bigroomCanvas.height = Math.round(h * dpr); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); lastW = w; lastH = h; } const cx = w / 2, cy = h / 2; const innerR = Math.min(w, h) * 0.14; const maxBarLen = Math.min(w, h) * 0.38; const bars = 36; const binStep = Math.floor(bigFreqData.length / bars); ctx.clearRect(0, 0, w, h); for (let i = 0; i < bars; i++) { const v = bigFreqData[i * binStep] / 255; if (v < 0.02) continue; const angle = (i / bars) * Math.PI * 2 - Math.PI / 2; const len = v * maxBarLen; const x1 = cx + Math.cos(angle) * innerR; const y1 = cy + Math.sin(angle) * innerR; const x2 = cx + Math.cos(angle) * (innerR + len); const y2 = cy + Math.sin(angle) * (innerR + len); const color = i < bars * 0.25 ? 'rgba(255,43,214,1)' : i < bars * 0.6 ? 'rgba(143,91,255,1)' : 'rgba(0,229,255,1)'; ctx.strokeStyle = color; ctx.lineWidth = 3; ctx.shadowColor = color; ctx.shadowBlur = 12; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); } ctx.shadowBlur = 0; } bigVizRaf = requestAnimationFrame(frame); } function stopBigViz() { if (bigVizRaf) { cancelAnimationFrame(bigVizRaf); bigVizRaf = null; } } // ── 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'; coverImg.removeAttribute('data-lightbox'); return; } if (coverImg.src === src) return; coverImg.style.opacity = '0'; coverImg.onload = () => { coverImg.style.opacity = '1'; coverImg.setAttribute('data-lightbox', ''); }; coverImg.onerror = () => { coverImg.style.opacity = '0'; coverImg.removeAttribute('data-lightbox'); }; coverImg.src = src; } function setCurrentTrack(song) { // Update cover (fallback to playlist cover if track has none). setCover(song.cover || (meta && meta.cover) || ''); if (!singleMode) { // 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 = ''; const creatorInfo = overlay.querySelector('#np-creator-info'); if (!creatorData) { creatorInfo.style.display = 'none'; return; } creatorInfo.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 || ''; if (creatorData.slug) { creatorInfo.style.cursor = 'pointer'; creatorInfo.onclick = () => { window.location.href = '/profiles/' + creatorData.slug; }; } else { creatorInfo.style.cursor = ''; creatorInfo.onclick = null; } } 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)); } 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; singleMode = !!(meta && meta.singleMode); overlay.classList.toggle('np-single', singleMode); if (singleMode) { playlistView.hidden = true; bigroomView.hidden = false; bigroomTitle.textContent = meta.title || ''; bigroomArtist.textContent = meta.artist || ''; bigroomMeta.textContent = meta.description || ''; // Delay viz start so canvas has layout dimensions setTimeout(startBigViz, 60); } else { playlistView.hidden = false; bigroomView.hidden = true; stopBigViz(); 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(' · '); if (queue.length) buildTrackList(); } populateCreator(meta.creator || null); applyLoginGuard(); syncRepeat(); syncPlayPause(); setCover(meta.cover || ''); 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; stopBigViz(); }, { once: true }); } 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; if (singleMode) setTimeout(startBigViz, 60); } function setReopenBtn(visible) { const btn = document.getElementById('np-reopen-btn'); if (!btn) return; if (visible) { if (btn.dataset.matchSongId) { // Song-detail page: only show when this exact song is playing. const playing = window.briTunesNowPlaying; visible = !!(playing && String(playing.id) === String(btn.dataset.matchSongId)); } else if (btn.dataset.matchPlaylistId) { // Playlist page: only show when this exact playlist is playing. visible = String(window.briTunesActivePlaylistId) === String(btn.dataset.matchPlaylistId); } else { visible = false; } } btn.style.display = visible ? '' : 'none'; } // ── Event wiring ─────────────────────────────────────────────────────────── document.addEventListener('click', (e) => { if (e.target.closest('#np-reopen-btn')) { reopen(); return; } const trackRow = e.target.closest('.np-track'); if (trackRow && isOpen) { const id = trackRow.dataset.trackId; if (id) window.dispatchEvent(new CustomEvent('briTunes:jumpto', { detail: { id } })); return; } const btn = e.target.closest('[data-nowplaying-meta]'); if (btn) { queueFromNpBtn = true; // Show directly — song hasn't started yet so the match-check would fail. const reopenBtn = document.getElementById('np-reopen-btn'); if (reopenBtn) reopenBtn.style.display = ''; 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(); }); window.addEventListener('briTunes:queue', (e) => { queueFromNpBtn = false; queue = e.detail.queue || []; if (isOpen && !singleMode) buildTrackList(); setReopenBtn(false); }); window.addEventListener('briTunes:play', (e) => { // Re-evaluate reopen button now that briTunesNowPlaying is updated. setReopenBtn(true); if (!isOpen) return; setCurrentTrack(e.detail); // If analyser just became available, kick off the viz if (singleMode && !bigVizRaf) startBigViz(); }); window.addEventListener('briTunes:navigate', () => { setReopenBtn(true); }); 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(); }); if (playerRep) { playerRep.addEventListener('click', () => { Promise.resolve().then(syncRepeat); }); } 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; }); })();