From a3ba79a9ce245974f25410a22ff0c7bded8ae6f4 Mon Sep 17 00:00:00 2001
From: Broque Thomas <26755000+Nezreka@users.noreply.github.com>
Date: Sun, 24 May 2026 11:02:19 -0700
Subject: [PATCH] Improve radio mode UI and behavior
Refactor and enhance the player radio feature: add npSetRadioMode, npQueueHasNext, and npEnsureCurrentTrackInQueue helpers to centralize radio-state changes and conditional radio fetch logic; replace direct npRadioMode toggles with npSetRadioMode in the expanded player and artist-radio flow (now awaits playLibraryTrack and triggers fetchIfNeeded). Add accessibility (aria-pressed) and label/pulse elements to the radio button, and update CSS for improved visuals and active-state animation. Also adjust toasts/messages and ensure the current library track is seeded into the queue when needed.
---
webui/index.html | 4 +-
webui/static/media-player.js | 81 +++++++++++++++++++------------
webui/static/stats-automations.js | 16 ++----
webui/static/style.css | 54 ++++++++++++++++-----
4 files changed, 99 insertions(+), 56 deletions(-)
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;