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:
parent
4c3375745c
commit
e652726c22
3 changed files with 177 additions and 1 deletions
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -39166,13 +39166,24 @@ async function _loadArtistTopTracks(artistName) {
|
|||
return n.toLocaleString();
|
||||
};
|
||||
|
||||
const _escAttr = (s) => (s || '').replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue