diff --git a/webui/index.html b/webui/index.html index 2486ed4b..43b1e265 100644 --- a/webui/index.html +++ b/webui/index.html @@ -7174,8 +7174,10 @@
-
diff --git a/webui/static/media-player.js b/webui/static/media-player.js index fd7eb526..cec97aba 100644 --- a/webui/static/media-player.js +++ b/webui/static/media-player.js @@ -1383,6 +1383,54 @@ let npMediaSource = null; let npVizAnimFrame = null; let npVizInitialized = false; +function npQueueHasNext() { + if (npQueue.length === 0) return false; + return npShuffleOn + ? npQueue.length > 1 + : (npQueueIndex < npQueue.length - 1 || npRepeatMode === 'all'); +} + +function npEnsureCurrentTrackInQueue() { + if (!currentTrack || !currentTrack.is_library || npQueue.length > 0) return; + npQueue.push({ + title: currentTrack.title, + artist: currentTrack.artist, + album: currentTrack.album, + file_path: currentTrack.filename || currentTrack.file_path, + filename: currentTrack.filename || currentTrack.file_path, + is_library: true, + image_url: currentTrack.image_url, + id: currentTrack.id, + artist_id: currentTrack.artist_id, + album_id: currentTrack.album_id, + bitrate: currentTrack.bitrate, + sample_rate: currentTrack.sample_rate + }); + npQueueIndex = 0; + renderNpQueue(); + updateNpPrevNextButtons(); +} + +function npSetRadioMode(enabled, options = {}) { + const { toast = true, fetchIfNeeded = false } = options; + npRadioMode = Boolean(enabled); + const radioBtn = document.getElementById('np-radio-btn'); + if (radioBtn) { + radioBtn.classList.toggle('active', npRadioMode); + radioBtn.setAttribute('aria-pressed', npRadioMode ? 'true' : 'false'); + radioBtn.title = npRadioMode + ? 'Radio mode on - similar tracks will auto-queue' + : 'Radio mode - auto-add similar tracks'; + } + if (toast) { + showToast(npRadioMode ? 'Radio mode on - similar tracks will auto-queue' : 'Radio mode off', 'success'); + } + if (npRadioMode && fetchIfNeeded && currentTrack && currentTrack.id && !npLoadingQueueItem && !npQueueHasNext()) { + npEnsureCurrentTrackInQueue(); + npFetchRadioTracks(); + } +} + function initExpandedPlayer() { const closeBtn = document.getElementById('np-close-btn'); const overlay = document.getElementById('np-modal-overlay'); @@ -1462,40 +1510,9 @@ function initExpandedPlayer() { const radioBtn = document.getElementById('np-radio-btn'); if (radioBtn) { radioBtn.addEventListener('click', () => { - npRadioMode = !npRadioMode; - radioBtn.classList.toggle('active', npRadioMode); - showToast(npRadioMode ? 'Radio mode on — similar tracks will auto-queue' : 'Radio mode off', 'success'); - // Immediately fetch radio tracks if turned on while playing with empty/exhausted queue - if (npRadioMode && currentTrack && currentTrack.id && !npLoadingQueueItem) { - const hasNext = npQueue.length > 0 && (npShuffleOn - ? npQueue.length > 1 - : (npQueueIndex < npQueue.length - 1 || npRepeatMode === 'all')); - if (!hasNext) { - // Add current track to queue first so it appears as "now playing" in context - if (npQueue.length === 0 && currentTrack.is_library) { - npQueue.push({ - title: currentTrack.title, - artist: currentTrack.artist, - album: currentTrack.album, - file_path: currentTrack.filename || currentTrack.file_path, - filename: currentTrack.filename || currentTrack.file_path, - is_library: true, - image_url: currentTrack.image_url, - id: currentTrack.id, - artist_id: currentTrack.artist_id, - album_id: currentTrack.album_id, - bitrate: currentTrack.bitrate - }); - npQueueIndex = 0; - renderNpQueue(); - updateNpPrevNextButtons(); - } - npFetchRadioTracks(); - } - } + npSetRadioMode(!npRadioMode, { fetchIfNeeded: true }); }); } - // Action link (Go to Artist) const gotoArtistBtn = document.getElementById('np-goto-artist'); if (gotoArtistBtn) { diff --git a/webui/static/stats-automations.js b/webui/static/stats-automations.js index 89b8d028..68772b35 100644 --- a/webui/static/stats-automations.js +++ b/webui/static/stats-automations.js @@ -6056,15 +6056,14 @@ async function playArtistRadio() { const albumArt = random.album.thumb_url || data.artist?.thumb_url || null; // Clear existing queue and disable radio before starting fresh - npRadioMode = false; + npSetRadioMode(false, { toast: false }); clearQueue(); if (audioPlayer && !audioPlayer.paused) { audioPlayer.pause(); } - // Play the track first, then enable radio mode after a short delay - // so currentTrack is set and the radio queue fill triggers - playLibraryTrack({ + // Play the track first so currentTrack is populated before radio seeds the queue. + await playLibraryTrack({ id: random.track.id, title: random.track.title, file_path: random.track.file_path, @@ -6073,14 +6072,9 @@ async function playArtistRadio() { album_id: random.album.id, }, random.album.title || '', artistName); - // Enable radio mode after track starts loading - setTimeout(() => { - npRadioMode = true; - const radioBtn = document.querySelector('.np-radio-btn'); - if (radioBtn) radioBtn.classList.add('active'); - }, 1000); + npSetRadioMode(true, { toast: false, fetchIfNeeded: true }); - showToast(`Playing ${artistName} radio — similar tracks will auto-queue`, 'success'); + showToast(`Playing ${artistName} radio - similar tracks will auto-queue`, 'success'); } catch (e) { showToast(`Failed to start artist radio: ${e.message}`, 'error'); } diff --git a/webui/static/style.css b/webui/static/style.css index 7d7a71a4..ea65ca7f 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -47281,26 +47281,56 @@ textarea.enhanced-meta-field-input { /* Radio mode button */ .np-radio-btn { - background: none; - border: 1px solid rgba(255, 255, 255, 0.1); - color: rgba(255, 255, 255, 0.35); + --np-radio-rgb: 79, 195, 247; + position: relative; + overflow: hidden; + background: + linear-gradient(135deg, rgba(var(--np-radio-rgb), 0.1), rgba(var(--np-radio-rgb), 0.025)), + rgba(255, 255, 255, 0.035); + border: 1px solid rgba(var(--np-radio-rgb), 0.22); + color: rgba(255, 255, 255, 0.62); font-size: 11px; - padding: 3px 8px; - border-radius: 6px; + font-weight: 650; + line-height: 1; + padding: 7px 10px; + border-radius: 999px; cursor: pointer; display: flex; align-items: center; - gap: 4px; - transition: all 0.15s ease; + gap: 6px; + min-height: 30px; + transition: all 0.18s ease; } .np-radio-btn:hover { - color: rgba(255, 255, 255, 0.7); - border-color: rgba(255, 255, 255, 0.2); + color: rgba(255, 255, 255, 0.9); + border-color: rgba(var(--np-radio-rgb), 0.42); + background: + linear-gradient(135deg, rgba(var(--np-radio-rgb), 0.16), rgba(var(--np-radio-rgb), 0.04)), + rgba(255, 255, 255, 0.055); } .np-radio-btn.active { - color: rgb(var(--accent-rgb)); - border-color: rgba(var(--accent-rgb), 0.3); - background: rgba(var(--accent-rgb), 0.08); + color: #ffffff; + border-color: rgba(var(--np-radio-rgb), 0.55); + background: + linear-gradient(135deg, rgba(var(--np-radio-rgb), 0.28), rgba(var(--np-radio-rgb), 0.07)), + rgba(255, 255, 255, 0.06); + box-shadow: 0 0 0 1px rgba(var(--np-radio-rgb), 0.08), 0 0 18px rgba(var(--np-radio-rgb), 0.12); +} +.np-radio-label { + position: relative; + z-index: 1; +} +.np-radio-pulse { + width: 6px; + height: 6px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.24); + box-shadow: none; + transition: all 0.18s ease; +} +.np-radio-btn.active .np-radio-pulse { + background: rgb(var(--np-radio-rgb)); + box-shadow: 0 0 12px rgba(var(--np-radio-rgb), 0.95); } .np-queue-header-actions { display: flex;