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.
This commit is contained in:
Broque Thomas 2026-05-24 11:02:19 -07:00
parent ccbe918808
commit a3ba79a9ce
4 changed files with 99 additions and 56 deletions

View file

@ -7174,8 +7174,10 @@
<span class="np-queue-count" id="np-queue-count"></span>
</button>
<div class="np-queue-header-actions">
<button class="np-radio-btn" id="np-radio-btn" title="Radio mode - auto-add similar tracks">
<button class="np-radio-btn" id="np-radio-btn" title="Radio mode - auto-add similar tracks" aria-pressed="false">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h.01"/><path d="M8.5 16.429a5 5 0 0 1 7 0"/><path d="M5 12.859a10 10 0 0 1 14 0"/><path d="M1.5 9.289a15 15 0 0 1 21 0"/></svg>
<span class="np-radio-label">Radio</span>
<span class="np-radio-pulse"></span>
</button>
<button class="np-queue-clear-btn" id="np-queue-clear" title="Clear queue">Clear</button>
</div>

View file

@ -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) {

View file

@ -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');
}

View file

@ -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;