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-icon">⚡</span>
|
||||||
<span class="enhance-text">Enhance Quality</span>
|
<span class="enhance-text">Enhance Quality</span>
|
||||||
</button>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Artist Hero Section -->
|
<!-- Artist Hero Section -->
|
||||||
|
|
|
||||||
|
|
@ -39166,13 +39166,24 @@ async function _loadArtistTopTracks(artistName) {
|
||||||
return n.toLocaleString();
|
return n.toLocaleString();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _escAttr = (s) => (s || '').replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||||
container.innerHTML = data.tracks.map((t, i) => `
|
container.innerHTML = data.tracks.map((t, i) => `
|
||||||
<div class="hero-top-track">
|
<div class="hero-top-track">
|
||||||
<span class="hero-top-track-num">${i + 1}</span>
|
<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>
|
<span class="hero-top-track-plays">${_fmtNum(t.playcount)}</span>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).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 = '';
|
sidebar.style.display = '';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.debug('Failed to load top tracks:', 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() {
|
function openEnhanceQualityModal() {
|
||||||
if (!_enhanceQualityData) return;
|
if (!_enhanceQualityData) return;
|
||||||
const data = _enhanceQualityData;
|
const data = _enhanceQualityData;
|
||||||
|
|
|
||||||
|
|
@ -16827,6 +16827,71 @@ body {
|
||||||
display: none;
|
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 Quality Modal ─── */
|
||||||
|
|
||||||
.enhance-modal-overlay {
|
.enhance-modal-overlay {
|
||||||
|
|
@ -19648,6 +19713,11 @@ body {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
opacity: 0.8;
|
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] {
|
.artist-hero-badge[data-url] {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
@ -19937,6 +20007,30 @@ body {
|
||||||
.hero-top-track:hover {
|
.hero-top-track:hover {
|
||||||
background: rgba(255, 255, 255, 0.04);
|
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 {
|
.hero-top-track-num {
|
||||||
font-size: 0.72em;
|
font-size: 0.72em;
|
||||||
color: rgba(255, 255, 255, 0.25);
|
color: rgba(255, 255, 255, 0.25);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue