Invert Tidal/Qobuz hero badges, add Artist Radio button and Last.fm play buttons

- Tidal and Qobuz SVG logos inverted on artist detail hero badges
- New Artist Radio button: clears queue, plays random artist track, enables radio
- Play buttons on Last.fm top tracks (hover reveal, resolves from library)
- Fixed inline JS escaping with data attribute delegation
This commit is contained in:
Broque Thomas 2026-03-23 20:42:33 -07:00
parent 4c3375745c
commit e652726c22
3 changed files with 177 additions and 1 deletions

View file

@ -2534,6 +2534,12 @@
<span class="enhance-icon"></span>
<span class="enhance-text">Enhance Quality</span>
</button>
<button class="library-artist-radio-btn" id="library-artist-radio-btn"
onclick="playArtistRadio()">
<span class="radio-icon">📻</span>
<span class="radio-text">Artist Radio</span>
</button>
</div>
<!-- Artist Hero Section -->

View file

@ -39166,13 +39166,24 @@ async function _loadArtistTopTracks(artistName) {
return n.toLocaleString();
};
const _escAttr = (s) => (s || '').replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
container.innerHTML = data.tracks.map((t, i) => `
<div class="hero-top-track">
<span class="hero-top-track-num">${i + 1}</span>
<span class="hero-top-track-name" title="${t.name}">${t.name}</span>
<button class="hero-top-track-play" data-track="${_escAttr(t.name)}" data-artist="${_escAttr(artistName)}" title="Play"></button>
<span class="hero-top-track-name" title="${_escAttr(t.name)}">${_escAttr(t.name)}</span>
<span class="hero-top-track-plays">${_fmtNum(t.playcount)}</span>
</div>
`).join('');
// Attach play handlers via delegation (avoids inline JS escaping issues)
container.onclick = (e) => {
const btn = e.target.closest('.hero-top-track-play');
if (btn) {
e.stopPropagation();
playStatsTrack(btn.dataset.track, btn.dataset.artist, '');
}
};
sidebar.style.display = '';
} catch (e) {
console.debug('Failed to load top tracks:', e);
@ -61569,6 +61580,71 @@ async function checkArtistEnhanceEligibility(artistId) {
}
}
async function playArtistRadio() {
try {
const artistId = artistDetailPageState.currentArtistId;
const artistName = artistDetailPageState.currentArtistName || '';
if (!artistId) {
showToast('No artist selected', 'error');
return;
}
// Get tracks from this artist's library
const resp = await fetch(`/api/library/artist/${artistId}/enhanced`);
if (!resp.ok) throw new Error('Failed to load artist data');
const data = await resp.json();
if (!data.success) throw new Error(data.error || 'Failed');
// Collect all tracks with file paths
const allTracks = [];
for (const album of (data.albums || [])) {
for (const track of (album.tracks || [])) {
if (track.file_path) {
allTracks.push({ track, album });
}
}
}
if (!allTracks.length) {
showToast('No playable tracks found for this artist', 'error');
return;
}
// Pick a random track
const random = allTracks[Math.floor(Math.random() * allTracks.length)];
const albumArt = random.album.thumb_url || data.artist?.thumb_url || null;
// Clear existing queue and disable radio before starting fresh
npRadioMode = 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({
id: random.track.id,
title: random.track.title,
file_path: random.track.file_path,
bitrate: random.track.bitrate,
artist_id: artistId,
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);
showToast(`Playing ${artistName} radio — similar tracks will auto-queue`, 'success');
} catch (e) {
showToast(`Failed to start artist radio: ${e.message}`, 'error');
}
}
function openEnhanceQualityModal() {
if (!_enhanceQualityData) return;
const data = _enhanceQualityData;

View file

@ -16827,6 +16827,71 @@ body {
display: none;
}
/* ─── Artist Radio Button ─── */
.library-artist-radio-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.02em;
color: rgba(255, 255, 255, 0.85);
background: linear-gradient(135deg, rgba(255, 255, 255, 0.06) 0%, rgba(255, 255, 255, 0.02) 100%);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
outline: none;
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
position: relative;
overflow: hidden;
backdrop-filter: blur(8px);
}
.library-artist-radio-btn::before {
content: '';
position: absolute;
inset: 0;
border-radius: 12px;
background: linear-gradient(135deg, rgba(var(--accent-rgb), 0.2) 0%, rgba(var(--accent-rgb), 0.05) 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
.library-artist-radio-btn:hover::before {
opacity: 1;
}
.library-artist-radio-btn:hover {
color: #ffffff;
border-color: rgba(var(--accent-rgb), 0.35);
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(var(--accent-rgb), 0.2), 0 0 0 1px rgba(var(--accent-rgb), 0.1);
}
.library-artist-radio-btn:active {
transform: translateY(0);
box-shadow: none;
}
.library-artist-radio-btn .radio-icon {
font-size: 14px;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
z-index: 1;
}
.library-artist-radio-btn:hover .radio-icon {
transform: scale(1.15);
}
.library-artist-radio-btn .radio-text {
position: relative;
z-index: 1;
}
/* ─── Enhance Quality Modal ─── */
.enhance-modal-overlay {
@ -19648,6 +19713,11 @@ body {
object-fit: contain;
opacity: 0.8;
}
/* Invert dark SVG logos for Tidal and Qobuz */
.artist-hero-badge[title="Tidal"] img,
.artist-hero-badge[title="Qobuz"] img {
filter: invert(1);
}
.artist-hero-badge[data-url] {
cursor: pointer;
}
@ -19937,6 +20007,30 @@ body {
.hero-top-track:hover {
background: rgba(255, 255, 255, 0.04);
}
.hero-top-track-play {
width: 20px;
height: 20px;
border-radius: 50%;
border: none;
background: rgba(var(--accent-rgb), 0.15);
color: rgb(var(--accent-rgb));
font-size: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
opacity: 0;
transition: all 0.2s;
}
.hero-top-track:hover .hero-top-track-play {
opacity: 1;
}
.hero-top-track-play:hover {
background: rgb(var(--accent-rgb));
color: #fff;
transform: scale(1.1);
}
.hero-top-track-num {
font-size: 0.72em;
color: rgba(255, 255, 255, 0.25);