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:
parent
ccbe918808
commit
a3ba79a9ce
4 changed files with 99 additions and 56 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue