refactor(webui): link artist detail navigation
- replace click-driven artist-detail hops with semantic links - keep SPA transitions via shell bridge interception for /artist-detail/:source/:id - drop legacy page helper wrappers and dead bridge plumbing
This commit is contained in:
parent
30c687ae7b
commit
0d683d87c0
13 changed files with 187 additions and 220 deletions
|
|
@ -2769,11 +2769,11 @@
|
|||
<!-- Popularity and genres will be populated here -->
|
||||
</div>
|
||||
<div class="discover-hero-actions">
|
||||
<button class="discover-hero-button secondary" id="discover-hero-discography"
|
||||
onclick="viewDiscoverHeroDiscography()">
|
||||
<a class="discover-hero-button secondary" id="discover-hero-discography" href="#"
|
||||
style="text-decoration:none;color:inherit;">
|
||||
<span class="button-icon">📀</span>
|
||||
<span class="button-text">View Discography</span>
|
||||
</button>
|
||||
</a>
|
||||
<button class="discover-hero-button primary watchlist-toggle-btn"
|
||||
id="discover-hero-add" onclick="toggleDiscoverHeroWatchlist(event)">
|
||||
<span class="watchlist-icon">👁️</span>
|
||||
|
|
@ -6977,10 +6977,11 @@
|
|||
<div class="np-format-badges" id="np-format-badges"></div>
|
||||
</div>
|
||||
<div class="np-action-buttons" id="np-action-buttons">
|
||||
<button class="np-action-btn" id="np-goto-artist" title="Go to Artist">
|
||||
<a class="np-action-btn" id="np-goto-artist" title="Go to Artist" href="#" tabindex="-1"
|
||||
style="text-decoration:none;color:inherit;pointer-events:none;">
|
||||
<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="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
<span>View Artist</span>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right: controls -->
|
||||
|
|
|
|||
8
webui/src/platform/shell/globals.d.ts
vendored
8
webui/src/platform/shell/globals.d.ts
vendored
|
|
@ -43,14 +43,6 @@ declare global {
|
|||
skipRouteChange?: boolean;
|
||||
},
|
||||
) => void;
|
||||
navigateToArtistDetailPage: (
|
||||
artistId: string | number,
|
||||
artistName: string,
|
||||
sourceOverride?: string | null,
|
||||
options?: {
|
||||
replace?: boolean;
|
||||
},
|
||||
) => void;
|
||||
cancelSimilarArtistsLoad: () => void;
|
||||
showReactHost: (pageId: ShellPageId) => void;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2316,6 +2316,25 @@ async function openWatchlistArtistDetailView(artistId, artistName) {
|
|||
if (artist.mood) metaTags.push(`<span class="watchlist-detail-genre-tag">${escapeHtml(artist.mood)}</span>`);
|
||||
if (artist.label) metaTags.push(`<span class="watchlist-detail-genre-tag">${escapeHtml(artist.label)}</span>`);
|
||||
|
||||
let discogId = null;
|
||||
let discogSource = null;
|
||||
const activeSrc = (currentMusicSourceName || '').toLowerCase();
|
||||
if (activeSrc.includes('spotify') && spotify_artist_id) {
|
||||
discogId = spotify_artist_id; discogSource = 'spotify';
|
||||
} else if (activeSrc.includes('discogs') && discogs_artist_id) {
|
||||
discogId = discogs_artist_id; discogSource = 'discogs';
|
||||
} else if (activeSrc.includes('deezer') && deezer_artist_id) {
|
||||
discogId = deezer_artist_id; discogSource = 'deezer';
|
||||
} else if (activeSrc.includes('musicbrainz') && musicbrainz_artist_id) {
|
||||
discogId = musicbrainz_artist_id; discogSource = 'musicbrainz';
|
||||
} else if (itunes_artist_id) {
|
||||
discogId = itunes_artist_id; discogSource = 'itunes';
|
||||
} else {
|
||||
discogId = spotify_artist_id || discogs_artist_id || deezer_artist_id || musicbrainz_artist_id || itunes_artist_id;
|
||||
discogSource = spotify_artist_id ? 'spotify' : discogs_artist_id ? 'discogs' : deezer_artist_id ? 'deezer' : musicbrainz_artist_id ? 'musicbrainz' : 'itunes';
|
||||
}
|
||||
const discogHref = discogId ? buildArtistDetailPath(discogId, discogSource) : '#';
|
||||
|
||||
overlay.innerHTML = `
|
||||
${artist.banner_url ? `
|
||||
<div class="watchlist-detail-banner">
|
||||
|
|
@ -2398,7 +2417,7 @@ async function openWatchlistArtistDetailView(artistId, artistName) {
|
|||
</div>
|
||||
|
||||
<div class="watchlist-detail-actions">
|
||||
<button class="watchlist-detail-discog-btn watchlist-detail-discog-action">View Discography</button>
|
||||
<a class="watchlist-detail-discog-btn watchlist-detail-discog-action" href="${discogHref}" ${discogId ? 'onclick="closeWatchlistArtistDetailView()"' : 'aria-disabled="true" tabindex="-1" style="pointer-events:none;opacity:0.5;text-decoration:none;color:inherit;"'}>View Discography</a>
|
||||
<button class="watchlist-detail-settings-btn watchlist-detail-settings-action">Settings</button>
|
||||
<button class="watchlist-detail-remove-btn watchlist-detail-remove-action">Remove from Watchlist</button>
|
||||
</div>
|
||||
|
|
@ -2410,30 +2429,6 @@ async function openWatchlistArtistDetailView(artistId, artistName) {
|
|||
closeWatchlistArtistDetailView();
|
||||
});
|
||||
|
||||
overlay.querySelector('.watchlist-detail-discog-action').addEventListener('click', () => {
|
||||
// Use the ID matching the active metadata source
|
||||
let discogId, source;
|
||||
const activeSrc = (currentMusicSourceName || '').toLowerCase();
|
||||
if (activeSrc.includes('spotify') && spotify_artist_id) {
|
||||
discogId = spotify_artist_id; source = 'spotify';
|
||||
} else if (activeSrc.includes('discogs') && discogs_artist_id) {
|
||||
discogId = discogs_artist_id; source = 'discogs';
|
||||
} else if (activeSrc.includes('deezer') && deezer_artist_id) {
|
||||
discogId = deezer_artist_id; source = 'deezer';
|
||||
} else if (activeSrc.includes('musicbrainz') && musicbrainz_artist_id) {
|
||||
discogId = musicbrainz_artist_id; source = 'musicbrainz';
|
||||
} else if (itunes_artist_id) {
|
||||
discogId = itunes_artist_id; source = 'itunes';
|
||||
} else {
|
||||
discogId = spotify_artist_id || discogs_artist_id || deezer_artist_id || musicbrainz_artist_id || itunes_artist_id;
|
||||
source = spotify_artist_id ? 'spotify' : discogs_artist_id ? 'discogs' : deezer_artist_id ? 'deezer' : musicbrainz_artist_id ? 'musicbrainz' : 'itunes';
|
||||
}
|
||||
if (discogId) {
|
||||
closeWatchlistArtistDetailView();
|
||||
navigateToArtistDetailPage(discogId, artistName, source);
|
||||
}
|
||||
});
|
||||
|
||||
overlay.querySelector('.watchlist-detail-settings-action').addEventListener('click', () => {
|
||||
// Remove overlay immediately so it doesn't block the config modal
|
||||
const detailOverlay = document.querySelector('.watchlist-artist-detail-overlay');
|
||||
|
|
|
|||
|
|
@ -269,10 +269,9 @@ function displayDiscoverHeroArtist(artist) {
|
|||
if (discographyBtn && artistId) {
|
||||
discographyBtn.setAttribute('data-artist-id', artistId);
|
||||
discographyBtn.setAttribute('data-artist-name', artist.artist_name);
|
||||
// Source the click handler will pass to navigateToArtistDetail. Without
|
||||
// this, source-only hero artists (which is the typical case — they
|
||||
// come from discover similar-artists, not the library) get looked up
|
||||
// as library IDs and 404. Backend always includes artist.source.
|
||||
discographyBtn.href = buildArtistDetailPath(artistId, artist.source || null);
|
||||
// Keep the source on the link so source-only hero artists resolve to
|
||||
// the correct artist-detail URL instead of being treated as library IDs.
|
||||
if (artist.source) discographyBtn.setAttribute('data-source', artist.source);
|
||||
else discographyBtn.removeAttribute('data-source');
|
||||
// Also store both IDs for cross-source operations
|
||||
|
|
@ -557,7 +556,7 @@ function renderRecommendedArtistsModal(modal, artists, source = null) {
|
|||
const similarText = artist.occurrence_count > 1
|
||||
? `Similar to ${artist.occurrence_count} in your watchlist`
|
||||
: 'Similar to an artist in your watchlist';
|
||||
const artistSource = artist.source || source || _recommendedArtistsSource || '';
|
||||
const artistSource = artist.source || source || _recommendedArtistsSource || '';
|
||||
return `
|
||||
<div class="recommended-artist-card"
|
||||
data-artist-name="${escapeHtml(artist.artist_name).toLowerCase()}"
|
||||
|
|
@ -568,21 +567,25 @@ function renderRecommendedArtistsModal(modal, artists, source = null) {
|
|||
data-artist-name="${escapeHtml(artist.artist_name)}">
|
||||
Add to Watchlist
|
||||
</button>
|
||||
<div class="recommended-card-image">
|
||||
${artist.image_url ? `
|
||||
<img src="${artist.image_url}"
|
||||
alt="${escapeHtml(artist.artist_name)}"
|
||||
loading="lazy"
|
||||
onerror="this.parentElement.innerHTML='<div class=\\'recommended-card-image-fallback\\'>🎤</div>';">
|
||||
` : `
|
||||
<div class="recommended-card-image-fallback">🎤</div>
|
||||
`}
|
||||
</div>
|
||||
<div class="recommended-card-info">
|
||||
<span class="recommended-card-name">${escapeHtml(artist.artist_name)}</span>
|
||||
<span class="recommended-card-similarity">${similarText}</span>
|
||||
<div class="recommended-card-genres">${genreTags}</div>
|
||||
</div>
|
||||
<a class="recommended-card-link" href="${buildArtistDetailPath(artist.artist_id, artistSource || null)}"
|
||||
onclick="closeRecommendedArtistsModal()"
|
||||
style="display:block;text-decoration:none;color:inherit;">
|
||||
<div class="recommended-card-image">
|
||||
${artist.image_url ? `
|
||||
<img src="${artist.image_url}"
|
||||
alt="${escapeHtml(artist.artist_name)}"
|
||||
loading="lazy"
|
||||
onerror="this.parentElement.innerHTML='<div class=\\'recommended-card-image-fallback\\'>🎤</div>';">
|
||||
` : `
|
||||
<div class="recommended-card-image-fallback">🎤</div>
|
||||
`}
|
||||
</div>
|
||||
<div class="recommended-card-info">
|
||||
<span class="recommended-card-name">${escapeHtml(artist.artist_name)}</span>
|
||||
<span class="recommended-card-similarity">${similarText}</span>
|
||||
<div class="recommended-card-genres">${genreTags}</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
|
|
@ -599,16 +602,6 @@ function renderRecommendedArtistsModal(modal, artists, source = null) {
|
|||
if (watchlistBtn) {
|
||||
e.stopPropagation();
|
||||
toggleRecommendedWatchlist(watchlistBtn);
|
||||
return;
|
||||
}
|
||||
|
||||
const card = e.target.closest('.recommended-artist-card');
|
||||
if (card) {
|
||||
const artistId = card.getAttribute('data-artist-id');
|
||||
const artistSource = card.getAttribute('data-artist-source') || _recommendedArtistsSource || null;
|
||||
const nameEl = card.querySelector('.recommended-card-name');
|
||||
const artistName = nameEl ? nameEl.textContent : '';
|
||||
viewRecommendedArtistDiscography(artistId, artistName, artistSource);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -739,11 +732,6 @@ async function checkRecommendedWatchlistStatuses(artists) {
|
|||
}
|
||||
}
|
||||
|
||||
async function viewRecommendedArtistDiscography(artistId, artistName, source = null) {
|
||||
closeRecommendedArtistsModal();
|
||||
navigateToArtistDetailPage(artistId, artistName, source || null);
|
||||
}
|
||||
|
||||
async function checkAllHeroWatchlistStatus() {
|
||||
const btn = document.getElementById('discover-hero-watch-all');
|
||||
if (!btn || !discoverHeroArtists || discoverHeroArtists.length === 0) return;
|
||||
|
|
@ -811,26 +799,6 @@ function jumpToDiscoverHeroSlide(index) {
|
|||
updateDiscoverHeroIndicators();
|
||||
}
|
||||
|
||||
async function viewDiscoverHeroDiscography() {
|
||||
const button = document.getElementById('discover-hero-discography');
|
||||
if (!button) return;
|
||||
|
||||
const artistId = button.getAttribute('data-artist-id');
|
||||
const artistName = button.getAttribute('data-artist-name');
|
||||
// Pass the source so /api/artist-detail knows to synthesize from that
|
||||
// metadata provider instead of doing a local DB lookup. Hero similar
|
||||
// artists are almost always source-only (not in the library).
|
||||
const source = button.getAttribute('data-source') || null;
|
||||
|
||||
if (!artistId || !artistName) {
|
||||
console.error('No artist data found for discography view');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`🎵 Navigating to artist detail for: ${artistName} (source: ${source || 'library'})`);
|
||||
navigateToArtistDetailPage(artistId, artistName, source);
|
||||
}
|
||||
|
||||
function showDiscoverHeroEmpty() {
|
||||
const titleEl = document.getElementById('discover-hero-title');
|
||||
const subtitleEl = document.getElementById('discover-hero-subtitle');
|
||||
|
|
@ -4514,10 +4482,7 @@ function _renderYourArtistCard(artist) {
|
|||
const detailSource = _pickArtistDetailSource(artist);
|
||||
const hasId = detailSource.id && detailSource.id !== '';
|
||||
|
||||
// Navigate to Artists page (name click) — source artist id, needs inline view
|
||||
const navAction = hasId
|
||||
? `event.stopPropagation(); navigateToArtistDetailPage('${escapeForInlineJs(detailSource.id)}', '${escapeForInlineJs(artist.artist_name)}', '${escapeForInlineJs(detailSource.source)}' || null)`
|
||||
: '';
|
||||
const detailHref = hasId ? buildArtistDetailPath(detailSource.id, detailSource.source) : '';
|
||||
|
||||
// Open info modal (card body click) — pass pool ID so we can look up all data
|
||||
const infoAction = hasId
|
||||
|
|
@ -4545,7 +4510,9 @@ function _renderYourArtistCard(artist) {
|
|||
<div class="ya-card-info-row">
|
||||
<div class="ya-origin-dots">${originDots}</div>
|
||||
</div>
|
||||
<div class="ya-card-name" ${navAction ? `onclick="${navAction}"` : ''}>${_esc(artist.artist_name)}</div>
|
||||
${hasId
|
||||
? `<a class="ya-card-name" href="${detailHref}" onclick="event.stopPropagation(); document.getElementById('ya-info-modal-overlay')?.remove(); document.getElementById('your-artists-modal-overlay')?.remove();" style="display:block;text-decoration:none;color:inherit;">${_esc(artist.artist_name)}</a>`
|
||||
: `<div class="ya-card-name">${_esc(artist.artist_name)}</div>`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -4695,10 +4662,10 @@ async function openYourArtistInfoModal(poolId) {
|
|||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<span>Explore</span>
|
||||
</button>
|
||||
<button class="ya-header-btn ya-viewall-btn" onclick="document.getElementById('ya-info-modal-overlay')?.remove(); document.getElementById('your-artists-modal-overlay')?.remove(); navigateToArtistDetailPage('${escapeForInlineJs(artistId)}', '${escapeForInlineJs(artistName)}', '${escapeForInlineJs(pool.active_source || '')}' || null)">
|
||||
<a class="ya-header-btn ya-viewall-btn" href="${buildArtistDetailPath(artistId, pool.active_source || null)}" onclick="document.getElementById('ya-info-modal-overlay')?.remove(); document.getElementById('your-artists-modal-overlay')?.remove();" style="text-decoration:none;color:inherit;">
|
||||
<span>View Discography</span>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></svg>
|
||||
</button>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
@ -6595,9 +6562,9 @@ function _artMapSetupInteraction(canvas) {
|
|||
<div class="artmap-ctx-item" onclick="_artMapHideContextMenu(); ${hasId ? `openYourArtistInfoModal_direct(${JSON.stringify(node).replace(/"/g, '"')})` : ''}">
|
||||
<span>ⓘ</span> Artist Info
|
||||
</div>
|
||||
<div class="artmap-ctx-item" onclick="_artMapHideContextMenu(); navigateToArtistDetailPage('${escapeForInlineJs(bestId)}', '${escapeForInlineJs(node.name)}', '${bestSource}' || null)">
|
||||
<a class="artmap-ctx-item" href="${bestId ? buildArtistDetailPath(bestId, bestSource) : '#'}" onclick="_artMapHideContextMenu()" ${bestId ? '' : 'aria-disabled="true" style="pointer-events:none;opacity:0.5;text-decoration:none;color:inherit;"'}>
|
||||
<span>💿</span> View Discography
|
||||
</div>
|
||||
</a>
|
||||
<div class="artmap-ctx-item" onclick="_artMapHideContextMenu(); toggleYourArtistWatchlist(0,'${escapeForInlineJs(node.name)}','${escapeForInlineJs(bestId)}','${bestSource}',null)">
|
||||
<span>👁</span> ${node.type === 'watchlist' ? 'On Watchlist' : 'Add to Watchlist'}
|
||||
</div>
|
||||
|
|
@ -7275,9 +7242,9 @@ async function openGenreDeepDive(genre) {
|
|||
// Always open on Artists page with discography — pass source for correct routing
|
||||
const imgUrl = _esc(a.image_url || '');
|
||||
const artSource = _esc(a.source || '');
|
||||
const clickAction = `onclick="document.getElementById('genre-deep-dive-modal').remove();navigateToArtistDetailPage('${_esc(a.entity_id)}','${_esc(a.name)}','${artSource}' || null)"`;
|
||||
const detailHref = a.entity_id ? buildArtistDetailPath(a.entity_id, a.source || null) : '#';
|
||||
const srcClass = (a.source || '').toLowerCase();
|
||||
return `<div class="genre-dive-artist" ${clickAction}>
|
||||
return `<a class="genre-dive-artist" href="${detailHref}" onclick="document.getElementById('genre-deep-dive-modal').remove()" style="text-decoration:none;color:inherit;">
|
||||
<div class="genre-dive-artist-img" style="${a.image_url ? `background-image:url('${_esc(a.image_url)}')` : ''}">
|
||||
${!a.image_url ? '<span>🎤</span>' : ''}
|
||||
</div>
|
||||
|
|
@ -7285,7 +7252,7 @@ async function openGenreDeepDive(genre) {
|
|||
<div class="genre-dive-artist-name">${_esc(a.name)}</div>
|
||||
${a.followers ? `<div class="genre-dive-artist-meta">${_fmtNum(a.followers)} followers</div>` : ''}
|
||||
${a.library_id ? '<div class="genre-dive-artist-badge">In Library</div>' : ''}
|
||||
</div>`;
|
||||
</a>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
|
|
|
|||
|
|
@ -633,48 +633,6 @@ async function openDownloadMissingModalForYouTube(virtualPlaylistId, playlistNam
|
|||
hideLoadingOverlay();
|
||||
}
|
||||
|
||||
function _navigateToArtistFromModal(artistId, artistName, imageUrl, source, playlistId) {
|
||||
if (!artistName) return;
|
||||
// Close the download modal
|
||||
const process = playlistId ? activeDownloadProcesses[playlistId] : null;
|
||||
const artistContext = process?.artist || {};
|
||||
const inferredSource = artistContext.spotify_artist_id ? 'spotify'
|
||||
: artistContext.itunes_artist_id ? 'itunes'
|
||||
: (artistContext.deezer_artist_id || artistContext.deezer_id) ? 'deezer'
|
||||
: (artistContext.discogs_artist_id || artistContext.discogs_id) ? 'discogs'
|
||||
: (artistContext.amazon_artist_id || artistContext.amazon_id) ? 'amazon'
|
||||
: (artistContext.soul_id || artistContext.hydrabase_artist_id) ? 'hydrabase'
|
||||
: null;
|
||||
const resolvedSource = source || process?.artist?.source || process?.album?.source || process?.source || inferredSource;
|
||||
const sourceKey = (resolvedSource || '').toString().toLowerCase();
|
||||
const sourceIdFields = {
|
||||
spotify: ['spotify_artist_id', 'id', 'artist_id'],
|
||||
itunes: ['itunes_artist_id', 'artist_id', 'id'],
|
||||
deezer: ['deezer_artist_id', 'deezer_id', 'artist_id', 'id'],
|
||||
discogs: ['discogs_artist_id', 'discogs_id', 'artist_id', 'id'],
|
||||
amazon: ['amazon_artist_id', 'amazon_id', 'artist_id', 'id'],
|
||||
hydrabase: ['soul_id', 'hydrabase_artist_id', 'artist_id', 'id'],
|
||||
musicbrainz: ['musicbrainz_id', 'artist_id', 'id'],
|
||||
};
|
||||
let resolvedArtistId = artistId;
|
||||
for (const field of (sourceIdFields[sourceKey] || ['artist_id', 'id'])) {
|
||||
const candidate = artistContext?.[field];
|
||||
if (candidate) {
|
||||
resolvedArtistId = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (resolvedArtistId && String(resolvedArtistId).toLowerCase() === String(artistName).toLowerCase()) {
|
||||
resolvedArtistId = null;
|
||||
}
|
||||
if (playlistId) closeDownloadMissingModal(playlistId);
|
||||
if (!resolvedArtistId || !resolvedSource) {
|
||||
showToast(`Artist details are not available for ${artistName}`, 'warning');
|
||||
return;
|
||||
}
|
||||
navigateToArtistDetailPage(resolvedArtistId, artistName, resolvedSource);
|
||||
}
|
||||
|
||||
async function closeDownloadMissingModal(playlistId) {
|
||||
const process = activeDownloadProcesses[playlistId];
|
||||
if (!process) {
|
||||
|
|
@ -5698,13 +5656,13 @@ function _gsRenderFromState(state) {
|
|||
|
||||
if (dbArtists.length) {
|
||||
h += '<div class="gsearch-section-header">📚 In Your Library</div><div class="gsearch-grid">';
|
||||
h += dbArtists.map(a => `<div class="gsearch-item" onclick="_gsClickArtist('${a.id}', '${_escAttr(a.name)}', true)"><div class="gsearch-item-art">${a.image_url ? `<img src="${a.image_url}" loading="lazy" onerror="this.parentElement.textContent='🎤'">` : '🎤'}</div><div class="gsearch-item-info"><div class="gsearch-item-title">${_escToast(a.name)}</div><div class="gsearch-item-sub">Library</div></div></div>`).join('');
|
||||
h += dbArtists.map(a => `<a class="gsearch-item" href="${a.id ? buildArtistDetailPath(a.id, null) : '#'}" onclick="_gsDeactivate()" style="text-decoration:none;color:inherit;">${a.image_url ? `<div class="gsearch-item-art"><img src="${a.image_url}" loading="lazy" onerror="this.parentElement.textContent='🎤'"></div>` : '<div class="gsearch-item-art">🎤</div>'}<div class="gsearch-item-info"><div class="gsearch-item-title">${_escToast(a.name)}</div><div class="gsearch-item-sub">Library</div></div></a>`).join('');
|
||||
h += '</div>';
|
||||
}
|
||||
|
||||
if (artists.length) {
|
||||
h += `<div class="gsearch-section-header">🎤 Artists <span class="gsearch-source-badge">${srcLabel}</span></div><div class="gsearch-grid" id="gsearch-artists-grid">`;
|
||||
h += artists.map(a => `<div class="gsearch-item" onclick="_gsClickArtist('${a.id}', '${_escAttr(a.name)}', false)" ${!a.image_url ? `data-artist-id="${a.id}" data-needs-image="true" data-artist-name="${_escAttr(a.name)}"` : ''}><div class="gsearch-item-art">${a.image_url ? `<img src="${a.image_url}" loading="lazy" onerror="this.parentElement.textContent='🎤'">` : '🎤'}</div><div class="gsearch-item-info"><div class="gsearch-item-title">${_escToast(a.name)}</div></div></div>`).join('');
|
||||
h += artists.map(a => `<a class="gsearch-item" href="${a.id ? buildArtistDetailPath(a.id, activeSrc || null) : '#'}" onclick="_gsDeactivate()" ${!a.image_url ? `data-artist-id="${a.id}" data-needs-image="true" data-artist-name="${_escAttr(a.name)}"` : ''} style="text-decoration:none;color:inherit;">${a.image_url ? `<div class="gsearch-item-art"><img src="${a.image_url}" loading="lazy" onerror="this.parentElement.textContent='🎤'"></div>` : '<div class="gsearch-item-art">🎤</div>'}<div class="gsearch-item-info"><div class="gsearch-item-title">${_escToast(a.name)}</div></div></a>`).join('');
|
||||
h += '</div>';
|
||||
}
|
||||
|
||||
|
|
@ -5778,13 +5736,6 @@ async function _gsLazyLoadArtistImages() {
|
|||
}
|
||||
}
|
||||
|
||||
function _gsClickArtist(id, name, isLibrary) {
|
||||
_gsDeactivate();
|
||||
const activeSource = _gsController && _gsController.state.activeSource;
|
||||
const source = isLibrary ? null : (activeSource || null);
|
||||
navigateToArtistDetailPage(id, name, source);
|
||||
}
|
||||
|
||||
async function _gsClickAlbum(albumId, albumName, artistName, imageUrl, source) {
|
||||
_gsDeactivate();
|
||||
// Same flow as handleEnhancedSearchAlbumClick — fetch album, open download modal
|
||||
|
|
|
|||
|
|
@ -218,6 +218,7 @@ function displayLibraryArtists(artists) {
|
|||
// Ignore clicks on badge icons (they open external links / toggle watchlist)
|
||||
const badge = e.target.closest('.source-card-icon');
|
||||
if (badge) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const url = badge.dataset.url;
|
||||
if (url) { window.open(url, '_blank'); return; }
|
||||
|
|
@ -233,10 +234,6 @@ function displayLibraryArtists(artists) {
|
|||
}
|
||||
return;
|
||||
}
|
||||
const card = e.target.closest('.library-artist-card');
|
||||
if (card) {
|
||||
navigateToArtistDetailPage(card.dataset.artistId, card.dataset.artistName);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -299,14 +296,14 @@ function buildLibraryArtistCardHTML(artist, index) {
|
|||
// Track stats
|
||||
const trackStat = artist.track_count > 0 ? `<span class="library-artist-stat">${artist.track_count} track${artist.track_count !== 1 ? 's' : ''}</span>` : '';
|
||||
|
||||
return `<div class="library-artist-card" data-artist-id="${_esc(String(artist.id))}" data-artist-name="${_esc(artist.name)}" style="position:relative;animation:cardFadeIn 0.35s cubic-bezier(0.4,0,0.2,1) ${delay}ms both">
|
||||
return `<a class="library-artist-card" href="${buildArtistDetailPath(artist.id)}" data-artist-id="${_esc(String(artist.id))}" data-artist-name="${_esc(artist.name)}" style="position:relative;display:block;animation:cardFadeIn 0.35s cubic-bezier(0.4,0,0.2,1) ${delay}ms both;text-decoration:none;color:inherit;">
|
||||
${badgeContainerHTML}
|
||||
${imageHTML}
|
||||
<div class="library-artist-info">
|
||||
<h3 class="library-artist-name" title="${_esc(artist.name)}">${_esc(artist.name)}</h3>
|
||||
<div class="library-artist-stats">${trackStat}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
</a>`;
|
||||
}
|
||||
|
||||
function updateLibraryPagination(pagination) {
|
||||
|
|
@ -780,27 +777,6 @@ if (typeof window !== 'undefined') {
|
|||
}
|
||||
|
||||
|
||||
// Public navigation entrypoint for artist detail. Callers should use this so
|
||||
// artist-detail navigations stay URL-driven, while the renderer handoff below
|
||||
// remains the legacy implementation detail.
|
||||
function navigateToArtistDetailPage(artistId, artistName, sourceOverride = null, options = {}) {
|
||||
const normalizedSource = sourceOverride || null;
|
||||
|
||||
if (!artistId) return false;
|
||||
if (currentPage === 'artist-detail' &&
|
||||
String(artistId) === String(artistDetailPageState.currentArtistId) &&
|
||||
String(normalizedSource || '') === String(artistDetailPageState.currentArtistSource || '')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return navigateToPage('artist-detail', {
|
||||
artistId,
|
||||
artistSource: normalizedSource,
|
||||
forceReload: true,
|
||||
replace: options.replace === true,
|
||||
});
|
||||
}
|
||||
|
||||
function navigateToArtistDetail(artistId, artistName, sourceOverride = null, options = {}) {
|
||||
const normalizedSource = sourceOverride || null;
|
||||
|
||||
|
|
|
|||
|
|
@ -107,6 +107,21 @@ function setTrackInfo(track) {
|
|||
document.getElementById('no-track-message').classList.add('hidden');
|
||||
document.getElementById('media-player').classList.remove('idle');
|
||||
|
||||
const gotoArtistBtn = document.getElementById('np-goto-artist');
|
||||
if (gotoArtistBtn) {
|
||||
if (track.artist_id) {
|
||||
gotoArtistBtn.href = buildArtistDetailPath(track.artist_id, track.artist_source || null);
|
||||
gotoArtistBtn.style.pointerEvents = '';
|
||||
gotoArtistBtn.setAttribute('aria-disabled', 'false');
|
||||
gotoArtistBtn.tabIndex = 0;
|
||||
} else {
|
||||
gotoArtistBtn.href = '#';
|
||||
gotoArtistBtn.style.pointerEvents = 'none';
|
||||
gotoArtistBtn.setAttribute('aria-disabled', 'true');
|
||||
gotoArtistBtn.tabIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Sync expanded player and media session
|
||||
updateNpTrackInfo();
|
||||
updateMediaSessionMetadata();
|
||||
|
|
@ -184,6 +199,14 @@ function clearTrack() {
|
|||
document.getElementById('no-track-message').classList.remove('hidden');
|
||||
document.getElementById('media-player').classList.add('idle');
|
||||
|
||||
const gotoArtistBtn = document.getElementById('np-goto-artist');
|
||||
if (gotoArtistBtn) {
|
||||
gotoArtistBtn.href = '#';
|
||||
gotoArtistBtn.style.pointerEvents = 'none';
|
||||
gotoArtistBtn.setAttribute('aria-disabled', 'true');
|
||||
gotoArtistBtn.tabIndex = -1;
|
||||
}
|
||||
|
||||
// Reset queue state
|
||||
npQueue = [];
|
||||
npQueueIndex = -1;
|
||||
|
|
@ -1231,15 +1254,11 @@ function initExpandedPlayer() {
|
|||
});
|
||||
}
|
||||
|
||||
// Action button (Go to Artist)
|
||||
// Action link (Go to Artist)
|
||||
const gotoArtistBtn = document.getElementById('np-goto-artist');
|
||||
if (gotoArtistBtn) {
|
||||
gotoArtistBtn.addEventListener('click', () => {
|
||||
if (currentTrack && currentTrack.artist_id) {
|
||||
closeNowPlayingModal();
|
||||
navigateToArtistDetailPage(currentTrack.artist_id, currentTrack.artist || '');
|
||||
}
|
||||
});
|
||||
gotoArtistBtn.style.textDecoration = 'none';
|
||||
gotoArtistBtn.style.color = 'inherit';
|
||||
}
|
||||
// Buffering state listeners on audioPlayer
|
||||
if (audioPlayer) {
|
||||
|
|
|
|||
|
|
@ -317,11 +317,7 @@ function initializeSearchModeToggle() {
|
|||
name: artist.name,
|
||||
meta: 'In Your Library',
|
||||
badge: { text: 'Library', class: 'enh-badge-library' },
|
||||
onClick: () => {
|
||||
console.log(`🎵 Opening library artist detail: ${artist.name} (ID: ${artist.id})`);
|
||||
hideDropdown();
|
||||
navigateToArtistDetailPage(artist.id, artist.name);
|
||||
}
|
||||
href: buildArtistDetailPath(artist.id),
|
||||
})
|
||||
);
|
||||
|
||||
|
|
@ -337,12 +333,7 @@ function initializeSearchModeToggle() {
|
|||
name: artist.name,
|
||||
meta: 'Artist',
|
||||
badge: sourceBadge,
|
||||
onClick: () => {
|
||||
const sourceOverride = searchController.state.activeSource;
|
||||
console.log(`🎵 Opening artist detail: ${artist.name} (ID: ${artist.id}, source: ${sourceOverride})`);
|
||||
hideDropdown();
|
||||
navigateToArtistDetailPage(artist.id, artist.name, sourceOverride || null);
|
||||
}
|
||||
href: buildArtistDetailPath(artist.id, searchController.state.activeSource || null),
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -545,7 +545,8 @@ function renderCompactSection(sectionId, listId, countId, items, mapItem) {
|
|||
|
||||
items.forEach(item => {
|
||||
const config = mapItem(item);
|
||||
const elem = document.createElement('div');
|
||||
const isLink = isArtist && !!config.href;
|
||||
const elem = document.createElement(isLink ? 'a' : 'div');
|
||||
|
||||
// Add appropriate card class
|
||||
if (isArtist) {
|
||||
|
|
@ -566,6 +567,13 @@ function renderCompactSection(sectionId, listId, countId, items, mapItem) {
|
|||
elem.className = 'enh-compact-item track-item';
|
||||
}
|
||||
|
||||
if (isLink) {
|
||||
elem.href = config.href;
|
||||
elem.style.color = 'inherit';
|
||||
elem.style.textDecoration = 'none';
|
||||
elem.setAttribute('aria-label', config.name || 'Artist');
|
||||
}
|
||||
|
||||
// Build image HTML with type-specific classes
|
||||
let imageClass = 'enh-item-image';
|
||||
let placeholderClass = 'enh-item-image-placeholder';
|
||||
|
|
@ -612,7 +620,9 @@ function renderCompactSection(sectionId, listId, countId, items, mapItem) {
|
|||
${badgeHtml}
|
||||
`;
|
||||
|
||||
elem.addEventListener('click', config.onClick);
|
||||
if (config.onClick) {
|
||||
elem.addEventListener('click', config.onClick);
|
||||
}
|
||||
|
||||
// Add play button handler for tracks
|
||||
if (isTrack && config.onPlay) {
|
||||
|
|
@ -3823,8 +3833,12 @@ function displaySimilarArtists(artists) {
|
|||
*/
|
||||
function createSimilarArtistBubble(artist) {
|
||||
// Create bubble container
|
||||
const bubble = document.createElement('div');
|
||||
const bubble = document.createElement('a');
|
||||
bubble.className = 'similar-artist-bubble';
|
||||
bubble.href = buildArtistDetailPath(artist.id, artist.source || null);
|
||||
bubble.style.color = 'inherit';
|
||||
bubble.style.textDecoration = 'none';
|
||||
bubble.setAttribute('aria-label', artist.name);
|
||||
bubble.setAttribute('data-artist-id', artist.id);
|
||||
bubble.setAttribute('data-artist-source', artist.source || '');
|
||||
if (artist.plugin) {
|
||||
|
|
@ -3877,13 +3891,6 @@ function createSimilarArtistBubble(artist) {
|
|||
bubble.appendChild(genres);
|
||||
}
|
||||
|
||||
// Click → navigate to the standalone artist-detail page. Works for both
|
||||
// library and source artists thanks to the source-aware backend endpoint.
|
||||
bubble.addEventListener('click', () => {
|
||||
console.log(`🎵 Clicked similar artist: ${artist.name} (ID: ${artist.id})`);
|
||||
navigateToArtistDetailPage(artist.id, artist.name, artist.source || null);
|
||||
});
|
||||
|
||||
return bubble;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -165,7 +165,6 @@ window.SoulSyncWebShellBridge = {
|
|||
activateLegacyPath(pathname) {
|
||||
activateLegacyPath(pathname);
|
||||
},
|
||||
navigateToArtistDetailPage,
|
||||
navigateToArtistDetail,
|
||||
cancelSimilarArtistsLoad() {
|
||||
if (typeof cancelSimilarArtistsLoad === 'function') {
|
||||
|
|
@ -177,5 +176,47 @@ window.SoulSyncWebShellBridge = {
|
|||
},
|
||||
};
|
||||
|
||||
function _handleShellLinkClick(event) {
|
||||
if (event.defaultPrevented || event.button !== 0 || _isModifiedLinkClick(event)) return;
|
||||
|
||||
const anchor = event.target?.closest?.('a[href]');
|
||||
if (!anchor || (anchor.target && anchor.target !== '_self')) return;
|
||||
|
||||
const href = anchor.getAttribute('href');
|
||||
if (!href || href === '#' || href.startsWith('javascript:')) return;
|
||||
|
||||
const router = getWebRouter();
|
||||
if (!router?.navigateToPage) return;
|
||||
|
||||
const pathname = anchor.pathname || new URL(anchor.href, window.location.href).pathname;
|
||||
|
||||
if (pathname.startsWith('/artist-detail/')) {
|
||||
_handleArtistDetailLinkClick(event, pathname, router);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function _handleArtistDetailLinkClick(event, pathname, router) {
|
||||
const parts = pathname.split('/').filter(Boolean);
|
||||
if (parts.length < 3) return;
|
||||
|
||||
// Keep the semantic link, but hand the click back to TanStack so artist
|
||||
// detail navigations stay in the SPA when the router is available.
|
||||
const source = decodeURIComponent(parts[1] || '');
|
||||
const artistId = decodeURIComponent(parts.slice(2).join('/'));
|
||||
if (!source || !artistId) return;
|
||||
|
||||
event.preventDefault();
|
||||
void router.navigateToPage('artist-detail', {
|
||||
artistId,
|
||||
artistSource: source,
|
||||
});
|
||||
}
|
||||
|
||||
function _isModifiedLinkClick(event) {
|
||||
return event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
|
||||
}
|
||||
|
||||
window.addEventListener('popstate', syncActivePageFromLocation);
|
||||
document.addEventListener('click', _handleShellLinkClick, true);
|
||||
window.dispatchEvent(new CustomEvent(SHELL_BRIDGE_READY_EVENT));
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ async function loadStatsData() {
|
|||
<span class="stats-ranked-num">${i + 1}</span>
|
||||
${item.image_url ? `<img class="stats-ranked-img" src="${item.image_url}" alt="" onerror="this.style.display='none'">` : ''}
|
||||
<div class="stats-ranked-info">
|
||||
<div class="stats-ranked-name">${item.id ? `<a class="stats-artist-link" onclick="navigateToArtistDetailPage('${item.id}','${_esc(item.name).replace(/'/g, "\\'")}')">${_esc(item.name)}</a>` : _esc(item.name)}${item.soul_id && !String(item.soul_id).startsWith('soul_unnamed_') ? ' <img src="/static/trans2.png" style="width:12px;height:12px;vertical-align:middle;opacity:0.5;" title="SoulID">' : ''}</div>
|
||||
<div class="stats-ranked-name">${item.id ? `<a class="stats-artist-link" href="${buildArtistDetailPath(item.id, item.source || null)}">${_esc(item.name)}</a>` : _esc(item.name)}${item.soul_id && !String(item.soul_id).startsWith('soul_unnamed_') ? ' <img src="/static/trans2.png" style="width:12px;height:12px;vertical-align:middle;opacity:0.5;" title="SoulID">' : ''}</div>
|
||||
<div class="stats-ranked-meta">${item.global_listeners ? _fmt(item.global_listeners) + ' global listeners' : ''}</div>
|
||||
</div>
|
||||
<span class="stats-ranked-count">${_fmt(item.play_count)} plays</span>
|
||||
|
|
@ -176,7 +176,7 @@ async function loadStatsData() {
|
|||
${item.image_url ? `<img class="stats-ranked-img" src="${item.image_url}" alt="" onerror="this.style.display='none'">` : ''}
|
||||
<div class="stats-ranked-info">
|
||||
<div class="stats-ranked-name">${_esc(item.name)}</div>
|
||||
<div class="stats-ranked-meta">${item.artist_id ? `<a class="stats-artist-link" onclick="navigateToArtistDetailPage('${item.artist_id}','${_esc(item.artist || '').replace(/'/g, "\\'")}')">${_esc(item.artist || '')}</a>` : _esc(item.artist || '')}</div>
|
||||
<div class="stats-ranked-meta">${item.artist_id ? `<a class="stats-artist-link" href="${buildArtistDetailPath(item.artist_id, item.artist_source || null)}">${_esc(item.artist || '')}</a>` : _esc(item.artist || '')}</div>
|
||||
</div>
|
||||
<span class="stats-ranked-count">${_fmt(item.play_count)} plays</span>
|
||||
</div>
|
||||
|
|
@ -189,7 +189,7 @@ async function loadStatsData() {
|
|||
${item.image_url ? `<img class="stats-ranked-img" src="${item.image_url}" alt="" onerror="this.style.display='none'">` : ''}
|
||||
<div class="stats-ranked-info">
|
||||
<div class="stats-ranked-name">${_esc(item.name)}</div>
|
||||
<div class="stats-ranked-meta">${item.artist_id ? `<a class="stats-artist-link" onclick="navigateToArtistDetailPage('${item.artist_id}','${_esc(item.artist || '').replace(/'/g, "\\'")}')">${_esc(item.artist || '')}</a>` : _esc(item.artist || '')}${item.album ? ' · ' + _esc(item.album) : ''}</div>
|
||||
<div class="stats-ranked-meta">${item.artist_id ? `<a class="stats-artist-link" href="${buildArtistDetailPath(item.artist_id, item.artist_source || null)}">${_esc(item.artist || '')}</a>` : _esc(item.artist || '')}${item.album ? ' · ' + _esc(item.album) : ''}</div>
|
||||
</div>
|
||||
<button class="stats-play-btn" onclick="event.stopPropagation();playStatsTrack('${_esc(item.name).replace(/'/g, "\\'")}','${_esc(item.artist || '').replace(/'/g, "\\'")}','${_esc(item.album || '').replace(/'/g, "\\'")}')" title="Play">▶</button>
|
||||
<span class="stats-ranked-count">${_fmt(item.play_count)} plays</span>
|
||||
|
|
@ -231,7 +231,7 @@ function _renderTopArtistsVisual(artists) {
|
|||
${top5.map((a, i) => {
|
||||
const pct = Math.round((a.play_count / maxPlays) * 100);
|
||||
const size = 44 + (4 - i) * 6; // Largest first: 68, 62, 56, 50, 44
|
||||
return `<div class="stats-artist-bubble" onclick="${a.id ? `navigateToArtistDetailPage('${a.id}','${_esc(a.name).replace(/'/g, "\\\\'")}')` : ''}" style="cursor:${a.id ? 'pointer' : 'default'}">
|
||||
return `<a class="stats-artist-bubble" href="${a.id ? buildArtistDetailPath(a.id, a.source || null) : '#'}" style="cursor:${a.id ? 'pointer' : 'default'};text-decoration:none;color:inherit;">
|
||||
<div class="stats-bubble-img" style="width:${size}px;height:${size}px;${a.image_url ? `background-image:url('${a.image_url}')` : ''}">
|
||||
${!a.image_url ? `<span>${(a.name || '?')[0]}</span>` : ''}
|
||||
</div>
|
||||
|
|
@ -240,7 +240,7 @@ function _renderTopArtistsVisual(artists) {
|
|||
</div>
|
||||
<div class="stats-bubble-name">${_esc(a.name)}</div>
|
||||
<div class="stats-bubble-count">${_fmt(a.play_count)}</div>
|
||||
</div>`;
|
||||
</a>`;
|
||||
}).join('')}
|
||||
</div>`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16876,7 +16876,7 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.watchlist-detail-actions button {
|
||||
.watchlist-detail-actions > * {
|
||||
flex: 1;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
|
|
@ -16885,9 +16885,14 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, transform 0.1s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.watchlist-detail-actions button:active {
|
||||
.watchlist-detail-actions > *:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1993,6 +1993,28 @@ function generateDownloadModalHeroSection(context) {
|
|||
const artistImage = artist?.image_url || artist?.images?.[0]?.url;
|
||||
const albumImage = album?.image_url || album?.images?.[0]?.url;
|
||||
const artistSource = artist?.source || album?.source || context.source || '';
|
||||
const sourceKey = (artistSource || '').toString().toLowerCase();
|
||||
const sourceIdFields = {
|
||||
spotify: ['spotify_artist_id', 'id', 'artist_id'],
|
||||
itunes: ['itunes_artist_id', 'artist_id', 'id'],
|
||||
deezer: ['deezer_artist_id', 'deezer_id', 'artist_id', 'id'],
|
||||
discogs: ['discogs_artist_id', 'discogs_id', 'artist_id', 'id'],
|
||||
amazon: ['amazon_artist_id', 'amazon_id', 'artist_id', 'id'],
|
||||
hydrabase: ['soul_id', 'hydrabase_artist_id', 'artist_id', 'id'],
|
||||
musicbrainz: ['musicbrainz_id', 'artist_id', 'id'],
|
||||
};
|
||||
let detailArtistId = artist?.id || artist?.artist_id || '';
|
||||
for (const field of (sourceIdFields[sourceKey] || ['artist_id', 'id'])) {
|
||||
const candidate = artist?.[field];
|
||||
if (candidate) {
|
||||
detailArtistId = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (detailArtistId && String(detailArtistId).toLowerCase() === String(artist?.name || '').toLowerCase()) {
|
||||
detailArtistId = '';
|
||||
}
|
||||
const artistHref = detailArtistId ? buildArtistDetailPath(detailArtistId, artistSource || null) : '#';
|
||||
|
||||
// Use album image as background if available
|
||||
if (albumImage) {
|
||||
|
|
@ -2007,7 +2029,7 @@ function generateDownloadModalHeroSection(context) {
|
|||
</div>
|
||||
<div class="download-missing-modal-hero-metadata">
|
||||
<h1 class="download-missing-modal-hero-title">${escapeHtml(album.name || 'Unknown Album')}</h1>
|
||||
<div class="download-missing-modal-hero-subtitle">by <a href="#" class="hero-artist-link" onclick="event.preventDefault();_navigateToArtistFromModal('${escapeHtml(artist.id || '')}','${escapeForInlineJs(artist.name || '')}','${escapeHtml(artist.image_url || '')}','${escapeHtml(artistSource)}','${escapeHtml(context.playlistId || '')}')">${escapeHtml(artist.name || 'Unknown Artist')}</a></div>
|
||||
<div class="download-missing-modal-hero-subtitle">by <a href="${artistHref}" class="hero-artist-link" onclick="closeDownloadMissingModal('${escapeForInlineJs(context.playlistId || '')}')" style="text-decoration:none;color:inherit;">${escapeHtml(artist.name || 'Unknown Artist')}</a></div>
|
||||
<div class="download-missing-modal-hero-details">
|
||||
<span class="download-missing-modal-hero-detail">${album.album_type || 'Album'}</span>
|
||||
<span class="download-missing-modal-hero-detail">${trackCount} tracks</span>
|
||||
|
|
|
|||
Loading…
Reference in a new issue