feat: add big-room single-song overlay with animated visualizer
- Introduce new `#np-bigroom-view` overlay for single-song playback with centered layout and animated circular spectrum visualizer (`#np-viz-big`) - Refactor now-playing overlay to toggle between playlist mode and big-room mode based on `meta.singleMode` - Add real-time canvas-based frequency visualization using analyser node, exposed globally as `window.briTunesAnalyser` - Update song/playlist pages to support single-song playback via new `data-nowplaying-meta` attribute - Add smart "Now Playing" reopen button that only appears when matching song/playlist is active, with per-song or per-playlist matching
This commit is contained in:
parent
021d0694df
commit
ce287886aa
|
|
@ -1203,6 +1203,47 @@ a.detail-creator-card:hover {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- Big-room single-song overlay ---------- */
|
||||||
|
#np-bigroom-view {
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 1;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 1.25rem 1.5rem 0;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.np-single #np-bigroom-view { display: flex; }
|
||||||
|
.np-single #np-btn-next { visibility: hidden; }
|
||||||
|
|
||||||
|
#np-bigroom-title {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--fg);
|
||||||
|
text-shadow: var(--glow-cyan);
|
||||||
|
line-height: 1.3;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
#np-bigroom-artist {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--muted);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
#np-bigroom-meta {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--muted);
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
}
|
||||||
|
#np-viz-big {
|
||||||
|
width: 270px;
|
||||||
|
height: 270px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- Notifications ---------- */
|
/* ---------- Notifications ---------- */
|
||||||
.notif-nav-btn {
|
.notif-nav-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,25 @@
|
||||||
|
|
||||||
/* Right panel */
|
/* Right panel */
|
||||||
'<div id="np-right">' +
|
'<div id="np-right">' +
|
||||||
|
|
||||||
|
/* Playlist mode view */
|
||||||
|
'<div id="np-playlist-view">' +
|
||||||
'<div id="np-header">' +
|
'<div id="np-header">' +
|
||||||
'<div id="np-pl-title"></div>' +
|
'<div id="np-pl-title"></div>' +
|
||||||
'<div id="np-pl-meta"></div>' +
|
'<div id="np-pl-meta"></div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<ul id="np-tracks"></ul>' +
|
'<ul id="np-tracks"></ul>' +
|
||||||
|
'</div>' +
|
||||||
|
|
||||||
|
/* Single-song / big-room view */
|
||||||
|
'<div id="np-bigroom-view">' +
|
||||||
|
'<p id="np-bigroom-title"></p>' +
|
||||||
|
'<p id="np-bigroom-artist"></p>' +
|
||||||
|
'<p id="np-bigroom-meta"></p>' +
|
||||||
|
'<canvas id="np-viz-big"></canvas>' +
|
||||||
|
'</div>' +
|
||||||
|
|
||||||
|
/* Shared controls bar */
|
||||||
'<div id="np-bar">' +
|
'<div id="np-bar">' +
|
||||||
'<div id="np-progress-wrap"><div id="np-progress-fill"></div></div>' +
|
'<div id="np-progress-wrap"><div id="np-progress-fill"></div></div>' +
|
||||||
'<div id="np-controls">' +
|
'<div id="np-controls">' +
|
||||||
|
|
@ -65,6 +79,12 @@
|
||||||
const plTitle = overlay.querySelector('#np-pl-title');
|
const plTitle = overlay.querySelector('#np-pl-title');
|
||||||
const plMeta = overlay.querySelector('#np-pl-meta');
|
const plMeta = overlay.querySelector('#np-pl-meta');
|
||||||
const tracksList = overlay.querySelector('#np-tracks');
|
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 progressWrap = overlay.querySelector('#np-progress-wrap');
|
||||||
const progressFill = overlay.querySelector('#np-progress-fill');
|
const progressFill = overlay.querySelector('#np-progress-fill');
|
||||||
const npPlayPauseBtn = overlay.querySelector('#np-btn-playpause');
|
const npPlayPauseBtn = overlay.querySelector('#np-btn-playpause');
|
||||||
|
|
@ -78,7 +98,76 @@
|
||||||
let queue = [];
|
let queue = [];
|
||||||
let isOpen = false;
|
let isOpen = false;
|
||||||
let closing = false;
|
let closing = false;
|
||||||
let queueFromNpBtn = false; // true when current queue was started from [data-nowplaying-meta]
|
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 ────────────────────────────────────────────────────────────────
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||||
function esc(str) {
|
function esc(str) {
|
||||||
|
|
@ -121,14 +210,14 @@
|
||||||
// Update cover (fallback to playlist cover if track has none).
|
// Update cover (fallback to playlist cover if track has none).
|
||||||
setCover(song.cover || (meta && meta.cover) || '');
|
setCover(song.cover || (meta && meta.cover) || '');
|
||||||
|
|
||||||
|
if (!singleMode) {
|
||||||
// Highlight active row and scroll it into view.
|
// Highlight active row and scroll it into view.
|
||||||
tracksList.querySelectorAll('.np-track').forEach(li => {
|
tracksList.querySelectorAll('.np-track').forEach(li => {
|
||||||
const active = String(li.dataset.trackId) === String(song.id);
|
const active = String(li.dataset.trackId) === String(song.id);
|
||||||
li.classList.toggle('np-active', active);
|
li.classList.toggle('np-active', active);
|
||||||
if (active) {
|
if (active) li.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||||
li.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Update bottom-bar social buttons.
|
// Update bottom-bar social buttons.
|
||||||
const hasSong = !!song.id;
|
const hasSong = !!song.id;
|
||||||
|
|
@ -167,7 +256,6 @@
|
||||||
}
|
}
|
||||||
creatorName.textContent = creatorData.displayName || '';
|
creatorName.textContent = creatorData.displayName || '';
|
||||||
|
|
||||||
// Make the whole creator block a link if we have a profile slug.
|
|
||||||
if (creatorData.slug) {
|
if (creatorData.slug) {
|
||||||
creatorInfo.style.cursor = 'pointer';
|
creatorInfo.style.cursor = 'pointer';
|
||||||
creatorInfo.onclick = () => { window.location.href = '/profiles/' + creatorData.slug; };
|
creatorInfo.onclick = () => { window.location.href = '/profiles/' + creatorData.slug; };
|
||||||
|
|
@ -190,7 +278,6 @@
|
||||||
npRepeatBtn.setAttribute('aria-pressed', String(on));
|
npRepeatBtn.setAttribute('aria-pressed', String(on));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user is a guest, social buttons should redirect to login.
|
|
||||||
function applyLoginGuard() {
|
function applyLoginGuard() {
|
||||||
const needsLogin = playerLike && playerLike.hasAttribute('data-require-login');
|
const needsLogin = playerLike && playerLike.hasAttribute('data-require-login');
|
||||||
if (needsLogin) {
|
if (needsLogin) {
|
||||||
|
|
@ -207,24 +294,37 @@
|
||||||
if (closing) return;
|
if (closing) return;
|
||||||
meta = metaData;
|
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 || '';
|
plTitle.textContent = meta.title || '';
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (meta.trackCount) parts.push(meta.trackCount + ' track' + (meta.trackCount === 1 ? '' : 's'));
|
if (meta.trackCount) parts.push(meta.trackCount + ' track' + (meta.trackCount === 1 ? '' : 's'));
|
||||||
if (meta.description) parts.push(meta.description);
|
if (meta.description) parts.push(meta.description);
|
||||||
plMeta.textContent = parts.join(' · ');
|
plMeta.textContent = parts.join(' · ');
|
||||||
|
|
||||||
|
if (queue.length) buildTrackList();
|
||||||
|
}
|
||||||
|
|
||||||
populateCreator(meta.creator || null);
|
populateCreator(meta.creator || null);
|
||||||
applyLoginGuard();
|
applyLoginGuard();
|
||||||
syncRepeat();
|
syncRepeat();
|
||||||
syncPlayPause();
|
syncPlayPause();
|
||||||
|
|
||||||
// Set initial cover to playlist image; briTunes:play will update it per-track.
|
|
||||||
setCover(meta.cover || '');
|
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;
|
const cur = window.briTunesNowPlaying;
|
||||||
if (cur) setCurrentTrack(cur);
|
if (cur) setCurrentTrack(cur);
|
||||||
|
|
||||||
|
|
@ -244,10 +344,10 @@
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
closing = false;
|
closing = false;
|
||||||
|
stopBigViz();
|
||||||
}, { once: true });
|
}, { once: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-show overlay without re-initialising — used by the "Now Playing" button.
|
|
||||||
function reopen() {
|
function reopen() {
|
||||||
if (isOpen || closing || !meta) return;
|
if (isOpen || closing || !meta) return;
|
||||||
syncRepeat();
|
syncRepeat();
|
||||||
|
|
@ -257,33 +357,46 @@
|
||||||
overlay.classList.add('np-open');
|
overlay.classList.add('np-open');
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
isOpen = true;
|
isOpen = true;
|
||||||
|
if (singleMode) setTimeout(startBigViz, 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setReopenBtn(visible) {
|
function setReopenBtn(visible) {
|
||||||
const btn = document.getElementById('np-reopen-btn');
|
const btn = document.getElementById('np-reopen-btn');
|
||||||
if (btn) btn.style.display = visible ? '' : 'none';
|
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 ───────────────────────────────────────────────────────────
|
// ── Event wiring ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
// Re-open button
|
|
||||||
if (e.target.closest('#np-reopen-btn')) {
|
if (e.target.closest('#np-reopen-btn')) {
|
||||||
reopen();
|
reopen();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Click on a track row in the overlay — jump to that song.
|
|
||||||
const trackRow = e.target.closest('.np-track');
|
const trackRow = e.target.closest('.np-track');
|
||||||
if (trackRow && isOpen) {
|
if (trackRow && isOpen) {
|
||||||
const id = trackRow.dataset.trackId;
|
const id = trackRow.dataset.trackId;
|
||||||
if (id) window.dispatchEvent(new CustomEvent('briTunes:jumpto', { detail: { id } }));
|
if (id) window.dispatchEvent(new CustomEvent('briTunes:jumpto', { detail: { id } }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Play All / Shuffle All on a playlist page
|
|
||||||
const btn = e.target.closest('[data-nowplaying-meta]');
|
const btn = e.target.closest('[data-nowplaying-meta]');
|
||||||
if (btn) {
|
if (btn) {
|
||||||
queueFromNpBtn = true;
|
queueFromNpBtn = true;
|
||||||
setReopenBtn(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'))); }
|
try { open(JSON.parse(btn.getAttribute('data-nowplaying-meta'))); }
|
||||||
catch (err) { console.error('[now-playing]', err); }
|
catch (err) { console.error('[now-playing]', err); }
|
||||||
return;
|
return;
|
||||||
|
|
@ -294,31 +407,26 @@
|
||||||
closeBtn.addEventListener('click', close);
|
closeBtn.addEventListener('click', close);
|
||||||
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && isOpen) 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) => {
|
window.addEventListener('briTunes:queue', (e) => {
|
||||||
queueFromNpBtn = false;
|
queueFromNpBtn = false;
|
||||||
queue = e.detail.queue || [];
|
queue = e.detail.queue || [];
|
||||||
if (isOpen) buildTrackList();
|
if (isOpen && !singleMode) buildTrackList();
|
||||||
// Hide the reopen button; the click handler will re-show it if this came
|
|
||||||
// from a [data-nowplaying-meta] button.
|
|
||||||
setReopenBtn(false);
|
setReopenBtn(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Track changes.
|
|
||||||
window.addEventListener('briTunes:play', (e) => {
|
window.addEventListener('briTunes:play', (e) => {
|
||||||
|
// Re-evaluate reopen button now that briTunesNowPlaying is updated.
|
||||||
|
setReopenBtn(true);
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
setCurrentTrack(e.detail);
|
setCurrentTrack(e.detail);
|
||||||
|
// If analyser just became available, kick off the viz
|
||||||
|
if (singleMode && !bigVizRaf) startBigViz();
|
||||||
});
|
});
|
||||||
|
|
||||||
// After client-side navigation, restore the reopen button if a queue is active.
|
|
||||||
window.addEventListener('briTunes:navigate', () => {
|
window.addEventListener('briTunes:navigate', () => {
|
||||||
if (queue.length > 0) setReopenBtn(true);
|
setReopenBtn(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Next / Repeat — delegate to actual player bar buttons.
|
|
||||||
npPlayPauseBtn.addEventListener('click', () => {
|
npPlayPauseBtn.addEventListener('click', () => {
|
||||||
if (!audio) return;
|
if (!audio) return;
|
||||||
if (audio.paused || audio.ended) audio.play().catch(() => {});
|
if (audio.paused || audio.ended) audio.play().catch(() => {});
|
||||||
|
|
@ -332,16 +440,13 @@
|
||||||
npNextBtn.addEventListener('click', () => { if (playerNext) playerNext.click(); });
|
npNextBtn.addEventListener('click', () => { if (playerNext) playerNext.click(); });
|
||||||
npRepeatBtn.addEventListener('click', () => {
|
npRepeatBtn.addEventListener('click', () => {
|
||||||
if (playerRep) playerRep.click();
|
if (playerRep) playerRep.click();
|
||||||
// State update will be caught by the listener below.
|
|
||||||
});
|
});
|
||||||
if (playerRep) {
|
if (playerRep) {
|
||||||
playerRep.addEventListener('click', () => {
|
playerRep.addEventListener('click', () => {
|
||||||
// Use a microtask so the class has already been toggled by player.js.
|
|
||||||
Promise.resolve().then(syncRepeat);
|
Promise.resolve().then(syncRepeat);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Progress bar.
|
|
||||||
if (audio) {
|
if (audio) {
|
||||||
audio.addEventListener('timeupdate', () => {
|
audio.addEventListener('timeupdate', () => {
|
||||||
if (!isOpen || !audio.duration) return;
|
if (!isOpen || !audio.duration) return;
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ if (canvas && audio) {
|
||||||
src.connect(analyser);
|
src.connect(analyser);
|
||||||
analyser.connect(audioCtx.destination);
|
analyser.connect(audioCtx.destination);
|
||||||
hooked = true;
|
hooked = true;
|
||||||
|
window.briTunesAnalyser = analyser;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[player-viz] could not hook audio:', err.message);
|
console.warn('[player-viz] could not hook audio:', err.message);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,8 @@
|
||||||
</button>
|
</button>
|
||||||
<a href="/playlists" class="btn-link">← Back</a>
|
<a href="/playlists" class="btn-link">← Back</a>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" id="np-reopen-btn" class="btn-nowplaying" style="display:none">
|
<button type="button" id="np-reopen-btn" class="btn-nowplaying" style="display:none"
|
||||||
|
data-match-playlist-id="<%= playlist.id %>">
|
||||||
▶ Now Playing
|
▶ Now Playing
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,25 @@
|
||||||
<% if (song.year) { %><%= song.year %> · <% } %>
|
<% if (song.year) { %><%= song.year %> · <% } %>
|
||||||
<% if (song.durationSeconds) { %><%= Math.floor(song.durationSeconds/60) %>:<%= String(song.durationSeconds%60).padStart(2,'0') %><% } %>
|
<% if (song.durationSeconds) { %><%= Math.floor(song.durationSeconds/60) %>:<%= String(song.durationSeconds%60).padStart(2,'0') %><% } %>
|
||||||
</p>
|
</p>
|
||||||
|
<% const npMeta = {
|
||||||
|
singleMode: true,
|
||||||
|
title: song.title,
|
||||||
|
artist: song.artist,
|
||||||
|
trackCount: 1,
|
||||||
|
cover: song.coverPath ? '/media/' + song.coverPath : null,
|
||||||
|
description: [song.genre, song.year, song.durationSeconds ? Math.floor(song.durationSeconds/60) + ':' + String(song.durationSeconds%60).padStart(2,'0') : null].filter(Boolean).join(' · '),
|
||||||
|
creator: creator ? { displayName: creator.displayName, slug: creator.slug || null, avatarPath: creator.avatarPath || null } : null
|
||||||
|
}; %>
|
||||||
<button type="button" class="btn-play btn-primary"
|
<button type="button" class="btn-play btn-primary"
|
||||||
data-play-song="<%= JSON.stringify({ id: song.id, title: song.title, artist: song.artist, cover: song.coverPath ? '/media/' + song.coverPath : null, userLiked: !!song.userLiked, userFavorited: !!song.userFavorited, likeCount: song.likeCount || 0, favoriteCount: song.favoriteCount || 0 }) %>">
|
data-play-song="<%= JSON.stringify({ id: song.id, title: song.title, artist: song.artist, cover: song.coverPath ? '/media/' + song.coverPath : null, userLiked: !!song.userLiked, userFavorited: !!song.userFavorited, likeCount: song.likeCount || 0, favoriteCount: song.favoriteCount || 0 }) %>"
|
||||||
|
data-nowplaying-meta="<%= JSON.stringify(npMeta) %>">
|
||||||
▶ Play
|
▶ Play
|
||||||
</button>
|
</button>
|
||||||
<a href="/songs" class="btn-link">← Back to songs</a>
|
<a href="/songs" class="btn-link">← Back to songs</a>
|
||||||
|
<button type="button" id="np-reopen-btn" class="btn-nowplaying" style="display:none"
|
||||||
|
data-match-song-id="<%= song.id %>">
|
||||||
|
▶ Now Playing
|
||||||
|
</button>
|
||||||
|
|
||||||
<% const shareUrl = appBaseUrl + '/songs/' + song.slug; %>
|
<% const shareUrl = appBaseUrl + '/songs/' + song.slug; %>
|
||||||
<% const shareText = siteName + ': ' + song.title + ' by ' + song.artist; %>
|
<% const shareText = siteName + ': ' + song.title + ' by ' + song.artist; %>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue