From a8985b317f9af1107ef505ab06c15b9eda5cc82e Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Sat, 30 May 2026 14:17:07 -0700 Subject: [PATCH] Now Playing: fix squashed stop button + queue persistence + crafted entrance - Stop button fix: my round .np-btn { width/height 46px; border-radius:50% } override was also hitting .np-btn-stop (it carries both classes), squashing the 'Stop' text pill into a tiny circle. Exempted .np-btn.np-btn-stop back to an auto-width pill. - Queue persistence: npPersistQueue() (called from renderNpQueue, the single mutation hook) saves the queue to localStorage; npRestoreQueue() on init repopulates the panel on reload WITHOUT auto-playing (index reset to -1). Queue no longer vanishes on refresh. - Crafted entrance: controls stagger-fade/rise in when the modal opens (npRiseIn keyframe, delays cascading util->progress->controls->volume-> upnext). Art container excluded so its transform stays free for the play-scale. Frontend-only; Boulder verifying live. --- webui/static/media-player.js | 35 +++++++++++++++++++++++++++++++++++ webui/static/style.css | 28 ++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/webui/static/media-player.js b/webui/static/media-player.js index 3f7ac7c2..6eb0c586 100644 --- a/webui/static/media-player.js +++ b/webui/static/media-player.js @@ -55,6 +55,9 @@ function initializeMediaPlayer() { const miniNextBtn = document.getElementById('mini-next-btn'); if (miniPrevBtn) miniPrevBtn.addEventListener('click', (e) => { e.stopPropagation(); playPreviousInQueue(); }); if (miniNextBtn) miniNextBtn.addEventListener('click', (e) => { e.stopPropagation(); playNextInQueue(); }); + + // Restore a previously-saved queue (does not auto-play) + npRestoreQueue(); } function toggleMediaPlayerExpansion() { @@ -2283,6 +2286,38 @@ function renderNpQueue() { }); npUpdateUpNext(); + npPersistQueue(); +} + +// ── Queue persistence across page reloads (localStorage) ── +const NP_QUEUE_STORAGE_KEY = 'soulsync-np-queue'; + +function npPersistQueue() { + try { + if (!npQueue.length) { localStorage.removeItem(NP_QUEUE_STORAGE_KEY); return; } + localStorage.setItem(NP_QUEUE_STORAGE_KEY, JSON.stringify({ + queue: npQueue, + index: npQueueIndex, + savedAt: Date.now(), + })); + } catch (e) { /* quota / disabled storage — non-fatal */ } +} + +// Restore the saved queue into the panel WITHOUT auto-playing (the user +// reloaded; resume playback is their choice via clicking a row). +function npRestoreQueue() { + try { + const raw = localStorage.getItem(NP_QUEUE_STORAGE_KEY); + if (!raw) return; + const data = JSON.parse(raw); + if (data && Array.isArray(data.queue) && data.queue.length) { + npQueue = data.queue; + // Don't claim a track is "playing" on a fresh load — nothing is. + npQueueIndex = -1; + renderNpQueue(); + updateNpPrevNextButtons(); + } + } catch (e) { /* corrupt entry — ignore */ } } // ── Queue drag-to-reorder ── diff --git a/webui/static/style.css b/webui/static/style.css index 87b0e54c..ee2e0184 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -48673,6 +48673,22 @@ textarea.enhanced-meta-field-input { .np-btn-shuffle.active, .np-btn-repeat.active { color: rgb(var(--accent-light-rgb)) !important; } .np-btn-shuffle.active::after, .np-btn-repeat.active::after { background: rgb(var(--accent-light-rgb)) !important; } +/* Stop is a text pill, NOT a circular transport button — exempt it from the + round .np-btn sizing above (which was squashing "Stop" into a tiny circle). */ +.np-btn.np-btn-stop { + width: auto !important; height: auto !important; + border-radius: 999px !important; + padding: 8px 20px !important; + background: rgba(255,255,255,0.04) !important; + border: 1px solid rgba(255,255,255,0.08) !important; +} +.np-btn.np-btn-stop:hover { + background: rgba(255,60,60,0.1) !important; + border-color: rgba(255,60,60,0.25) !important; + color: rgba(255,90,90,0.9) !important; + transform: none !important; +} + /* ── Visualizer bars: accent gradient ── */ .np-viz-bar { background: linear-gradient(180deg, rgb(var(--accent-light-rgb)), rgba(var(--accent-rgb),0.35)) !important; @@ -48690,6 +48706,18 @@ textarea.enhanced-meta-field-input { .np-close-btn:hover { color: #fff !important; background: rgba(255,255,255,0.1) !important; } .np-lyrics-line.active { color: rgb(var(--accent-light-rgb)) !important; font-weight: 700 !important; } +/* ── Crafted entrance: controls stagger-fade in when modal opens ── + (Art container is intentionally excluded — its transform is reserved for the + play-scale; it gets its own scale-in via the box transition.) */ +.np-modal-overlay:not(.hidden) .np-track-info { animation: npRiseIn 0.55s cubic-bezier(0.16,1,0.3,1) 0.06s both; } +.np-modal-overlay:not(.hidden) .np-right > * { animation: npRiseIn 0.5s cubic-bezier(0.16,1,0.3,1) both; } +.np-modal-overlay:not(.hidden) .np-util-row { animation-delay: 0.08s; } +.np-modal-overlay:not(.hidden) .np-progress-section { animation-delay: 0.12s; } +.np-modal-overlay:not(.hidden) .np-controls-row { animation-delay: 0.16s; } +.np-modal-overlay:not(.hidden) .np-volume-row { animation-delay: 0.20s; } +.np-modal-overlay:not(.hidden) .np-upnext { animation-delay: 0.24s; } +@keyframes npRiseIn { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: translateY(0); } } + /* ── Click-art → music-synced visualizer takeover (Plexamp-style) ── */ .np-album-art-container { cursor: pointer; } .np-art-hint {