feat: add "Now Playing" overlay modal for playlist playback with persistent reopen button
- Introduce a full-screen "Now Playing" overlay modal with left (cover/creator) and right (track list/controls) panels - Add CSS styles for animated cyan-themed overlay, track list, progress bar, and equalizer visualizer - Integrate new `now-playing.js` module and update layout to include it - Refactor playlist action buttons to include a "Now Playing" reopen button that appears when playback starts - Dispatch `briTunes:queue` event on queue initialization for external UI synchronization
This commit is contained in:
parent
be5857fe3b
commit
a4c1595eaa
|
|
@ -175,6 +175,37 @@ a:hover { color: #fff; text-shadow: var(--glow-cyan); text-decoration: none; }
|
|||
.btn-link { color: var(--muted); margin-left: 0.75rem; font-size: 0.82rem; }
|
||||
.btn-link:hover { color: var(--neon-magenta); text-shadow: var(--glow-magenta); }
|
||||
|
||||
/* Playlist action row — Play All + Shuffle All on left, Now Playing on right */
|
||||
.playlist-btn-row {
|
||||
display: flex; align-items: center;
|
||||
justify-content: space-between; gap: 0.5rem;
|
||||
flex-wrap: wrap; margin: 0.25rem 0;
|
||||
}
|
||||
.playlist-btn-left { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
|
||||
|
||||
/* "Now Playing" re-open button — filled cyan to signal active state */
|
||||
.btn-nowplaying {
|
||||
background: var(--neon-cyan);
|
||||
color: #05060d;
|
||||
border: 1px solid var(--neon-cyan);
|
||||
padding: 0.55rem 1rem;
|
||||
border-radius: var(--radius);
|
||||
font-weight: 700;
|
||||
font-size: 0.82rem;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 14px rgba(0,229,255,0.6), 0 0 28px rgba(0,229,255,0.25);
|
||||
transition: transform .12s, box-shadow .15s;
|
||||
animation: np-btn-pulse 2.5s ease-in-out infinite;
|
||||
}
|
||||
.btn-nowplaying:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 0 22px rgba(0,229,255,0.9), 0 0 44px rgba(0,229,255,0.4);
|
||||
}
|
||||
@keyframes np-btn-pulse {
|
||||
0%, 100% { box-shadow: 0 0 14px rgba(0,229,255,0.6), 0 0 28px rgba(0,229,255,0.25); }
|
||||
50% { box-shadow: 0 0 20px rgba(0,229,255,0.85), 0 0 40px rgba(0,229,255,0.4); }
|
||||
}
|
||||
|
||||
button.linklike {
|
||||
background: none; border: none; color: var(--neon-cyan);
|
||||
padding: 0; cursor: pointer; font: inherit;
|
||||
|
|
@ -606,6 +637,182 @@ button.linklike.danger:hover { color: #fff; text-shadow: 0 0 6px var(--danger);
|
|||
}
|
||||
.lb-close:hover { background: rgba(255,255,255,0.15); border-color: var(--neon-cyan); }
|
||||
|
||||
/* ---------- Now Playing overlay ---------- */
|
||||
#np-overlay {
|
||||
display: none;
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,0);
|
||||
z-index: 2000;
|
||||
align-items: center; justify-content: center;
|
||||
}
|
||||
#np-overlay.np-open { display: flex; animation: lb-fade-in 0.25s ease forwards; }
|
||||
#np-overlay.np-closing { animation: lb-fade-out 0.2s ease forwards; }
|
||||
|
||||
#np-modal {
|
||||
position: relative; display: flex;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--neon-violet);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 0 0 1px var(--border), 0 0 40px rgba(143,91,255,0.45);
|
||||
width: min(940px, 92vw);
|
||||
height: min(640px, 85vh);
|
||||
overflow: hidden;
|
||||
}
|
||||
#np-overlay.np-open #np-modal { animation: lb-zoom-in 0.3s cubic-bezier(0.34, 1.4, 0.64, 1) forwards; }
|
||||
#np-overlay.np-closing #np-modal { animation: lb-zoom-out 0.2s ease forwards; }
|
||||
|
||||
#np-close {
|
||||
position: absolute; top: 10px; right: 10px; z-index: 10;
|
||||
width: 30px; height: 30px;
|
||||
background: rgba(0,0,0,0.65); border: 1px solid rgba(255,255,255,0.3);
|
||||
border-radius: 4px; color: #fff; font-size: 1.1rem;
|
||||
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
#np-close:hover { background: rgba(255,255,255,0.15); border-color: var(--neon-cyan); }
|
||||
|
||||
/* ── Left panel ──────────────────────────────────────────────────── */
|
||||
#np-left {
|
||||
width: 38%; flex-shrink: 0;
|
||||
display: flex; flex-direction: column;
|
||||
background: #000; overflow: hidden;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* 1:1 cover using padding-bottom trick */
|
||||
#np-cover-wrap {
|
||||
width: 100%; position: relative; flex-shrink: 0;
|
||||
}
|
||||
#np-cover-wrap::before { content: ''; display: block; padding-bottom: 100%; }
|
||||
#np-cover {
|
||||
position: absolute; inset: 0;
|
||||
width: 100%; height: 100%; object-fit: cover;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
/* Creator info below the square cover */
|
||||
#np-creator-info {
|
||||
flex: 1; display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
gap: 0.4rem; padding: 0.9rem 1rem;
|
||||
background: var(--bg-inset); overflow: hidden; text-align: center;
|
||||
}
|
||||
.np-creator-avatar {
|
||||
width: 58px; height: 58px; border-radius: 50%;
|
||||
object-fit: cover; border: 2px solid var(--border);
|
||||
background: var(--bg-panel); flex-shrink: 0;
|
||||
}
|
||||
.np-creator-placeholder {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.5rem; font-weight: bold; color: var(--muted);
|
||||
}
|
||||
.np-creator-label {
|
||||
font-size: 0.62rem; color: var(--muted);
|
||||
text-transform: uppercase; letter-spacing: 0.07em; margin: 0;
|
||||
}
|
||||
.np-creator-name { font-size: 0.82rem; color: var(--fg); font-weight: 600; margin: 0; }
|
||||
|
||||
/* ── Right panel ─────────────────────────────────────────────────── */
|
||||
#np-right { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
||||
|
||||
#np-header {
|
||||
padding: 0.9rem 1.25rem 0.8rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-inset); flex-shrink: 0;
|
||||
}
|
||||
#np-pl-title {
|
||||
font-family: 'Orbitron', monospace;
|
||||
font-size: 0.88rem; font-weight: 700; color: var(--fg);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
#np-pl-meta { font-size: 0.7rem; color: var(--muted); margin-top: 0.2rem; }
|
||||
|
||||
/* Track list */
|
||||
#np-tracks {
|
||||
list-style: none; padding: 0; margin: 0;
|
||||
overflow-y: auto; flex: 1;
|
||||
scrollbar-width: thin; scrollbar-color: var(--border) transparent;
|
||||
}
|
||||
#np-tracks::-webkit-scrollbar { width: 5px; }
|
||||
#np-tracks::-webkit-scrollbar-track { background: transparent; }
|
||||
#np-tracks::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
||||
|
||||
.np-track {
|
||||
display: flex; align-items: center; gap: 0.55rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.04);
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.np-track:hover { background: rgba(255,255,255,0.04); }
|
||||
.np-track.np-active { background: rgba(0,229,255,0.07); }
|
||||
|
||||
/* Equalizer bars */
|
||||
.np-eq {
|
||||
display: none; align-items: flex-end; gap: 2px;
|
||||
height: 14px; flex-shrink: 0;
|
||||
}
|
||||
.np-track.np-active .np-eq { display: inline-flex; }
|
||||
.np-eq span {
|
||||
width: 3px; background: var(--neon-cyan);
|
||||
border-radius: 1px; transform-origin: bottom;
|
||||
}
|
||||
.np-eq span:nth-child(1) { height: 14px; animation: np-eq 0.55s ease-in-out infinite alternate; }
|
||||
.np-eq span:nth-child(2) { height: 10px; animation: np-eq 0.40s ease-in-out infinite alternate; animation-delay: -0.15s; }
|
||||
.np-eq span:nth-child(3) { height: 12px; animation: np-eq 0.65s ease-in-out infinite alternate; animation-delay: -0.35s; }
|
||||
@keyframes np-eq { from { transform: scaleY(0.15); opacity: 0.5; } to { transform: scaleY(1); opacity: 1; } }
|
||||
|
||||
.np-track-num {
|
||||
font-size: 0.7rem; color: var(--muted);
|
||||
width: 1.5rem; text-align: right; flex-shrink: 0;
|
||||
font-family: 'Orbitron', monospace;
|
||||
}
|
||||
.np-track.np-active .np-track-num { color: var(--neon-cyan); }
|
||||
.np-track-info { flex: 1; min-width: 0; }
|
||||
.np-track-title {
|
||||
font-size: 0.84rem; color: var(--fg);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.np-track-artist {
|
||||
font-size: 0.7rem; color: var(--muted);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.np-track.np-active .np-track-title { color: var(--neon-cyan); }
|
||||
.np-track.np-active .np-track-artist { color: rgba(0,229,255,0.6); }
|
||||
|
||||
/* Bottom bar */
|
||||
#np-bar { flex-shrink: 0; border-top: 1px solid var(--border); background: var(--bg-inset); }
|
||||
#np-progress-wrap {
|
||||
height: 4px; background: rgba(255,255,255,0.08); cursor: pointer;
|
||||
transition: height 0.15s;
|
||||
}
|
||||
#np-progress-wrap:hover { height: 6px; }
|
||||
#np-progress-fill {
|
||||
height: 100%; width: 0%;
|
||||
background: var(--neon-cyan);
|
||||
box-shadow: var(--glow-cyan);
|
||||
transition: width 0.25s linear;
|
||||
}
|
||||
#np-controls {
|
||||
display: flex; align-items: center; gap: 0.2rem;
|
||||
padding: 0.45rem 0.75rem;
|
||||
}
|
||||
#np-btn-playpause, #np-btn-next, #np-btn-repeat {
|
||||
background: none; border: none; color: var(--muted);
|
||||
cursor: pointer; font-size: 1rem; padding: 3px 6px;
|
||||
border-radius: 4px; transition: color 0.15s, background 0.15s;
|
||||
line-height: 1;
|
||||
}
|
||||
#np-btn-playpause { font-size: 1.1rem; color: var(--fg); padding: 3px 8px; }
|
||||
#np-btn-playpause:hover { color: var(--neon-cyan); background: rgba(0,229,255,0.08); }
|
||||
#np-btn-next:hover,
|
||||
#np-btn-repeat:hover { color: var(--fg); background: rgba(255,255,255,0.06); }
|
||||
#np-btn-repeat.active { color: var(--neon-cyan); text-shadow: var(--glow-cyan); }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
#np-left { display: none; }
|
||||
#np-modal { width: 96vw; height: 90vh; }
|
||||
}
|
||||
|
||||
/* ---------- Avatar ---------- */
|
||||
.avatar-section { display: flex; align-items: center; gap: 1rem; }
|
||||
.avatar-preview {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,324 @@
|
|||
(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 =
|
||||
'<div id="np-modal">' +
|
||||
'<button id="np-close" aria-label="Close">\u00d7</button>' +
|
||||
|
||||
/* Left panel */
|
||||
'<div id="np-left">' +
|
||||
'<div id="np-cover-wrap"><img id="np-cover" src="" alt=""></div>' +
|
||||
'<div id="np-creator-info">' +
|
||||
'<div id="np-creator-avatar-wrap"></div>' +
|
||||
'<p class="np-creator-label">Created by</p>' +
|
||||
'<p class="np-creator-name" id="np-creator-name"></p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
/* Right panel */
|
||||
'<div id="np-right">' +
|
||||
'<div id="np-header">' +
|
||||
'<div id="np-pl-title"></div>' +
|
||||
'<div id="np-pl-meta"></div>' +
|
||||
'</div>' +
|
||||
'<ul id="np-tracks"></ul>' +
|
||||
'<div id="np-bar">' +
|
||||
'<div id="np-progress-wrap"><div id="np-progress-fill"></div></div>' +
|
||||
'<div id="np-controls">' +
|
||||
'<button id="np-btn-playpause" title="Play / Pause">\u25b6</button>' +
|
||||
'<button id="np-btn-next" title="Next track">\u23ed</button>' +
|
||||
'<button id="np-btn-repeat" title="Repeat">\u21ba</button>' +
|
||||
'<button class="btn-social" data-social-action="like" data-social-type="song" data-social-id="" aria-pressed="false" title="Like" disabled>' +
|
||||
'\ud83d\udc4d <span class="social-count">0</span>' +
|
||||
'</button>' +
|
||||
'<button class="btn-social" data-social-action="favorite" data-social-type="song" data-social-id="" aria-pressed="false" title="Favorite" disabled>' +
|
||||
'\u2b50 <span class="social-count">0</span>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
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, '>').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 =
|
||||
'<span class="np-eq" aria-hidden="true"><span></span><span></span><span></span></span>' +
|
||||
'<span class="np-track-num">' + (i + 1) + '</span>' +
|
||||
'<span class="np-track-info">' +
|
||||
'<div class="np-track-title">' + esc(track.title || '') + '</div>' +
|
||||
'<div class="np-track-artist">' + esc(track.artist || '') + '</div>' +
|
||||
'</span>';
|
||||
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;
|
||||
});
|
||||
})();
|
||||
|
|
@ -77,6 +77,7 @@
|
|||
if (!Array.isArray(list) || list.length === 0) return;
|
||||
state.queue = list;
|
||||
state.index = 0;
|
||||
window.dispatchEvent(new CustomEvent('briTunes:queue', { detail: { queue: list.slice() } }));
|
||||
playCurrent();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
<script src="/static/js/player.js" defer></script>
|
||||
<script src="/static/js/social.js" defer></script>
|
||||
<script src="/static/js/lightbox.js" defer></script>
|
||||
<script src="/static/js/now-playing.js" defer></script>
|
||||
<script type="module" src="/static/js/bg-grid.js"></script>
|
||||
<script type="module" src="/static/js/player-viz.js"></script>
|
||||
<script type="module" src="/static/js/boot-sequence.js"></script>
|
||||
|
|
|
|||
|
|
@ -23,17 +23,27 @@
|
|||
aria-pressed="<%= playlist.userFavorited ? 'true' : 'false' %>"
|
||||
title="Favorite">⭐ <span class="social-count"><%= playlist.favoriteCount || 0 %></span></button>
|
||||
</div>
|
||||
<% const npMeta = JSON.stringify({ title: playlist.title, description: playlist.description || null, cover: playlist.coverPath ? '/media/' + playlist.coverPath : null, trackCount: tracks.length, creator: creator ? { displayName: creator.displayName, avatarPath: creator.avatarPath || null } : null }); %>
|
||||
<div class="playlist-btn-row">
|
||||
<div class="playlist-btn-left">
|
||||
<button type="button" class="btn-primary"
|
||||
id="play-playlist-btn"
|
||||
data-nowplaying-meta="<%= npMeta %>"
|
||||
data-play-playlist="<%= JSON.stringify(tracks.map((t) => ({ id: t.id, title: t.title, artist: t.artist, cover: t.coverPath ? '/media/' + t.coverPath : null, userLiked: !!t.userLiked, userFavorited: !!t.userFavorited, likeCount: t.likeCount || 0, favoriteCount: t.favoriteCount || 0 }))) %>">
|
||||
▶ Play all
|
||||
</button>
|
||||
<button type="button" class="btn-primary"
|
||||
data-nowplaying-meta="<%= npMeta %>"
|
||||
data-shuffle-playlist="<%= JSON.stringify(tracks.map((t) => ({ id: t.id, title: t.title, artist: t.artist, cover: t.coverPath ? '/media/' + t.coverPath : null, userLiked: !!t.userLiked, userFavorited: !!t.userFavorited, likeCount: t.likeCount || 0, favoriteCount: t.favoriteCount || 0 }))) %>">
|
||||
⇄ Shuffle all
|
||||
</button>
|
||||
<a href="/playlists" class="btn-link">← Back</a>
|
||||
</div>
|
||||
<button type="button" id="np-reopen-btn" class="btn-nowplaying" style="display:none">
|
||||
▶ Now Playing
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% if (creator) { %>
|
||||
<div class="detail-creator-card">
|
||||
<% if (creator.avatarPath) { %>
|
||||
|
|
|
|||
Loading…
Reference in New Issue