refactor(webui): centralize artist-detail handoff
- add a canonical TanStack route for artist-detail and keep the legacy page as the renderer target - expose page-level artist-detail navigation on the shell bridge for legacy callers - remove artist-detail-specific routing, origin stack, and back-label logic from the shared shell helpers
This commit is contained in:
parent
728481db31
commit
5e39f1ee09
13 changed files with 52 additions and 123 deletions
|
|
@ -2418,7 +2418,7 @@
|
|||
<div class="page" id="artist-detail-page">
|
||||
<div class="page-header">
|
||||
<button class="back-btn" id="artist-detail-back-btn">
|
||||
<span>← Back to Library</span>
|
||||
<span>← Back</span>
|
||||
</button>
|
||||
|
||||
<button class="library-artist-watchlist-btn" id="library-artist-watchlist-btn">
|
||||
|
|
|
|||
9
webui/src/platform/shell/globals.d.ts
vendored
9
webui/src/platform/shell/globals.d.ts
vendored
|
|
@ -40,10 +40,17 @@ declare global {
|
|||
artistName: string,
|
||||
sourceOverride?: string | null,
|
||||
options?: {
|
||||
skipOriginPush?: boolean;
|
||||
skipRouteChange?: boolean;
|
||||
},
|
||||
) => void;
|
||||
navigateToArtistDetailPage: (
|
||||
artistId: string | number,
|
||||
artistName: string,
|
||||
sourceOverride?: string | null,
|
||||
options?: {
|
||||
replace?: boolean;
|
||||
},
|
||||
) => void;
|
||||
showReactHost: (pageId: ShellPageId) => void;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ function ArtistDetailPage() {
|
|||
|
||||
const normalizedSource = source.toLowerCase() === 'library' ? null : source.toLowerCase();
|
||||
bridge.navigateToArtistDetail(id, '', normalizedSource, {
|
||||
skipOriginPush: true,
|
||||
skipRouteChange: true,
|
||||
});
|
||||
}, [bridge, id, source]);
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ describe('artist-detail route', () => {
|
|||
'',
|
||||
'spotify',
|
||||
{
|
||||
skipOriginPush: true,
|
||||
skipRouteChange: true,
|
||||
},
|
||||
);
|
||||
|
|
@ -67,7 +66,6 @@ describe('artist-detail route', () => {
|
|||
'',
|
||||
null,
|
||||
{
|
||||
skipOriginPush: true,
|
||||
skipRouteChange: true,
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2430,7 +2430,7 @@ async function openWatchlistArtistDetailView(artistId, artistName) {
|
|||
}
|
||||
if (discogId) {
|
||||
closeWatchlistArtistDetailView();
|
||||
navigateToArtistDetail(discogId, artistName, source);
|
||||
navigateToArtistDetailPage(discogId, artistName, source);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -741,7 +741,7 @@ async function checkRecommendedWatchlistStatuses(artists) {
|
|||
|
||||
async function viewRecommendedArtistDiscography(artistId, artistName, source = null) {
|
||||
closeRecommendedArtistsModal();
|
||||
navigateToArtistDetail(artistId, artistName, source || null);
|
||||
navigateToArtistDetailPage(artistId, artistName, source || null);
|
||||
}
|
||||
|
||||
async function checkAllHeroWatchlistStatus() {
|
||||
|
|
@ -828,7 +828,7 @@ async function viewDiscoverHeroDiscography() {
|
|||
}
|
||||
|
||||
console.log(`🎵 Navigating to artist detail for: ${artistName} (source: ${source || 'library'})`);
|
||||
navigateToArtistDetail(artistId, artistName, source);
|
||||
navigateToArtistDetailPage(artistId, artistName, source);
|
||||
}
|
||||
|
||||
function showDiscoverHeroEmpty() {
|
||||
|
|
@ -4516,7 +4516,7 @@ function _renderYourArtistCard(artist) {
|
|||
|
||||
// Navigate to Artists page (name click) — source artist id, needs inline view
|
||||
const navAction = hasId
|
||||
? `event.stopPropagation(); navigateToArtistDetail('${escapeForInlineJs(detailSource.id)}', '${escapeForInlineJs(artist.artist_name)}', '${escapeForInlineJs(detailSource.source)}' || null)`
|
||||
? `event.stopPropagation(); navigateToArtistDetailPage('${escapeForInlineJs(detailSource.id)}', '${escapeForInlineJs(artist.artist_name)}', '${escapeForInlineJs(detailSource.source)}' || null)`
|
||||
: '';
|
||||
|
||||
// Open info modal (card body click) — pass pool ID so we can look up all data
|
||||
|
|
@ -4695,7 +4695,7 @@ 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(); navigateToArtistDetail('${escapeForInlineJs(artistId)}', '${escapeForInlineJs(artistName)}', '${escapeForInlineJs(pool.active_source || '')}' || null)">
|
||||
<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)">
|
||||
<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>
|
||||
|
|
@ -6595,7 +6595,7 @@ 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(); navigateToArtistDetail('${escapeForInlineJs(bestId)}', '${escapeForInlineJs(node.name)}', '${bestSource}' || null)">
|
||||
<div class="artmap-ctx-item" onclick="_artMapHideContextMenu(); navigateToArtistDetailPage('${escapeForInlineJs(bestId)}', '${escapeForInlineJs(node.name)}', '${bestSource}' || null)">
|
||||
<span>💿</span> View Discography
|
||||
</div>
|
||||
<div class="artmap-ctx-item" onclick="_artMapHideContextMenu(); toggleYourArtistWatchlist(0,'${escapeForInlineJs(node.name)}','${escapeForInlineJs(bestId)}','${bestSource}',null)">
|
||||
|
|
@ -7275,7 +7275,7 @@ 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();navigateToArtistDetail('${_esc(a.entity_id)}','${_esc(a.name)}','${artSource}' || null)"`;
|
||||
const clickAction = `onclick="document.getElementById('genre-deep-dive-modal').remove();navigateToArtistDetailPage('${_esc(a.entity_id)}','${_esc(a.name)}','${artSource}' || null)"`;
|
||||
const srcClass = (a.source || '').toLowerCase();
|
||||
return `<div class="genre-dive-artist" ${clickAction}>
|
||||
<div class="genre-dive-artist-img" style="${a.image_url ? `background-image:url('${_esc(a.image_url)}')` : ''}">
|
||||
|
|
@ -8768,4 +8768,3 @@ if (document.readyState === 'loading') {
|
|||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
|
|
|
|||
|
|
@ -672,7 +672,7 @@ function _navigateToArtistFromModal(artistId, artistName, imageUrl, source, play
|
|||
showToast(`Artist details are not available for ${artistName}`, 'warning');
|
||||
return;
|
||||
}
|
||||
navigateToArtistDetail(resolvedArtistId, artistName, resolvedSource);
|
||||
navigateToArtistDetailPage(resolvedArtistId, artistName, resolvedSource);
|
||||
}
|
||||
|
||||
async function closeDownloadMissingModal(playlistId) {
|
||||
|
|
@ -5782,7 +5782,7 @@ function _gsClickArtist(id, name, isLibrary) {
|
|||
_gsDeactivate();
|
||||
const activeSource = _gsController && _gsController.state.activeSource;
|
||||
const source = isLibrary ? null : (activeSource || null);
|
||||
navigateToArtistDetail(id, name, source);
|
||||
navigateToArtistDetailPage(id, name, source);
|
||||
}
|
||||
|
||||
async function _gsClickAlbum(albumId, albumName, artistName, imageUrl, source) {
|
||||
|
|
@ -6400,4 +6400,3 @@ const additionalStyles = `
|
|||
document.head.insertAdjacentHTML('beforeend', additionalStyles);
|
||||
|
||||
// ============================================================================
|
||||
|
||||
|
|
|
|||
|
|
@ -235,7 +235,7 @@ function displayLibraryArtists(artists) {
|
|||
}
|
||||
const card = e.target.closest('.library-artist-card');
|
||||
if (card) {
|
||||
navigateToArtistDetail(card.dataset.artistId, card.dataset.artistName);
|
||||
navigateToArtistDetailPage(card.dataset.artistId, card.dataset.artistName);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -688,11 +688,6 @@ let artistDetailPageState = {
|
|||
currentArtistId: null,
|
||||
currentArtistName: null,
|
||||
currentArtistSource: null,
|
||||
// Stack of origins captured by navigateToArtistDetail for the back button.
|
||||
// Each entry is either {type:'page', pageId} or {type:'artist', id, name, source}
|
||||
// so chained navigation (Search → A → similar B → similar C) walks back one
|
||||
// step at a time instead of jumping straight to Search.
|
||||
originStack: [],
|
||||
enhancedView: false,
|
||||
enhancedData: null,
|
||||
expandedAlbums: new Set(),
|
||||
|
|
@ -710,7 +705,6 @@ function clearArtistDetailPageState() {
|
|||
artistDetailPageState.currentArtistId = null;
|
||||
artistDetailPageState.currentArtistName = null;
|
||||
artistDetailPageState.currentArtistSource = null;
|
||||
artistDetailPageState.originStack = [];
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
|
|
@ -786,21 +780,26 @@ if (typeof window !== 'undefined') {
|
|||
}
|
||||
|
||||
|
||||
// Friendly labels for the dynamic "← Back to X" button on the artist-detail page.
|
||||
// Page id (the value of currentPage) -> button label.
|
||||
const _ARTIST_DETAIL_BACK_LABELS = {
|
||||
library: 'Back to Library',
|
||||
search: 'Back to Search',
|
||||
discover: 'Back to Discover',
|
||||
watchlist: 'Back to Watchlist',
|
||||
wishlist: 'Back to Wishlist',
|
||||
stats: 'Back to Stats',
|
||||
'playlist-explorer': 'Back to Explorer',
|
||||
automations: 'Back to Automations',
|
||||
dashboard: 'Back to Dashboard',
|
||||
sync: 'Back to Sync',
|
||||
'active-downloads': 'Back to Downloads',
|
||||
};
|
||||
// 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;
|
||||
|
|
@ -817,49 +816,11 @@ function navigateToArtistDetail(artistId, artistName, sourceOverride = null, opt
|
|||
artistSource: normalizedSource,
|
||||
skipRouteChange: options.skipRouteChange === true
|
||||
});
|
||||
_updateArtistDetailBackButtonLabel();
|
||||
}
|
||||
return;
|
||||
}
|
||||
console.log(`🎵 Navigating to artist detail: ${artistName} (ID: ${artistId}${sourceOverride ? `, source: ${sourceOverride}` : ''})`);
|
||||
|
||||
// Capture the current location on the origin stack BEFORE navigateToPage
|
||||
// flips currentPage. The back button walks this stack one step at a time,
|
||||
// so a chain like Search → A → similar B → similar C steps back through
|
||||
// C → B → A → Search instead of jumping straight home. `skipOriginPush`
|
||||
// lets the back button re-enter a prior artist without re-pushing.
|
||||
if (!options.skipOriginPush) {
|
||||
// Fresh entry (from a non-artist page) starts a new chain; any stale
|
||||
// entries from a prior artist-detail visit are dropped.
|
||||
if (currentPage !== 'artist-detail') {
|
||||
artistDetailPageState.originStack = [];
|
||||
}
|
||||
|
||||
let entry;
|
||||
if (currentPage === 'artist-detail' && artistDetailPageState.currentArtistId) {
|
||||
entry = {
|
||||
type: 'artist',
|
||||
id: artistDetailPageState.currentArtistId,
|
||||
name: artistDetailPageState.currentArtistName,
|
||||
source: artistDetailPageState.currentArtistSource,
|
||||
};
|
||||
} else {
|
||||
const pageId = (typeof currentPage === 'string' && currentPage && currentPage !== 'artist-detail')
|
||||
? currentPage : 'library';
|
||||
entry = { type: 'page', pageId };
|
||||
}
|
||||
|
||||
// Avoid pushing a duplicate top entry on repeated clicks of the same target.
|
||||
const top = artistDetailPageState.originStack[artistDetailPageState.originStack.length - 1];
|
||||
const isDuplicate = top && top.type === entry.type && (
|
||||
(entry.type === 'page' && top.pageId === entry.pageId) ||
|
||||
(entry.type === 'artist' && String(top.id) === String(entry.id))
|
||||
);
|
||||
if (!isDuplicate) {
|
||||
artistDetailPageState.originStack.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// Abort any in-progress completion stream
|
||||
if (artistDetailPageState.completionController) {
|
||||
artistDetailPageState.completionController.abort();
|
||||
|
|
@ -909,9 +870,6 @@ function navigateToArtistDetail(artistId, artistName, sourceOverride = null, opt
|
|||
skipRouteChange: options.skipRouteChange === true
|
||||
});
|
||||
|
||||
// Update back-button label to reflect where the next pop will land.
|
||||
_updateArtistDetailBackButtonLabel();
|
||||
|
||||
// Initialize if needed and load data
|
||||
if (!artistDetailPageState.isInitialized) {
|
||||
initializeArtistDetailPage();
|
||||
|
|
@ -921,27 +879,11 @@ function navigateToArtistDetail(artistId, artistName, sourceOverride = null, opt
|
|||
loadArtistDetailData(artistId, artistName);
|
||||
}
|
||||
|
||||
function _updateArtistDetailBackButtonLabel() {
|
||||
const backBtnLabel = document.querySelector('#artist-detail-back-btn span');
|
||||
if (!backBtnLabel) return;
|
||||
const stack = artistDetailPageState.originStack || [];
|
||||
const top = stack[stack.length - 1];
|
||||
if (!top) {
|
||||
backBtnLabel.textContent = `← ${_ARTIST_DETAIL_BACK_LABELS.library}`;
|
||||
} else if (top.type === 'artist') {
|
||||
backBtnLabel.textContent = `← Back to ${top.name}`;
|
||||
} else {
|
||||
const friendly = _ARTIST_DETAIL_BACK_LABELS[top.pageId] || _ARTIST_DETAIL_BACK_LABELS.library;
|
||||
backBtnLabel.textContent = `← ${friendly}`;
|
||||
}
|
||||
}
|
||||
|
||||
function initializeArtistDetailPage() {
|
||||
console.log("🔧 Initializing Artist Detail page...");
|
||||
|
||||
// Initialize back button — pops the origin stack one step at a time so a
|
||||
// chain like Search → A → B → C walks back through C → B → A → Search
|
||||
// instead of jumping straight to the original entry page.
|
||||
// Initialize back button — use browser history when possible, with a
|
||||
// simple library fallback if the user lands here without in-app history.
|
||||
const backBtn = document.getElementById("artist-detail-back-btn");
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener("click", () => {
|
||||
|
|
@ -951,27 +893,12 @@ function initializeArtistDetailPage() {
|
|||
artistDetailPageState.completionController = null;
|
||||
}
|
||||
|
||||
const stack = artistDetailPageState.originStack || [];
|
||||
if (stack.length > 0) {
|
||||
const target = stack.pop();
|
||||
if (target.type === 'artist') {
|
||||
// Re-enter a prior artist in the chain without re-pushing,
|
||||
// so the stack keeps shrinking as the user steps back.
|
||||
navigateToArtistDetail(target.id, target.name, target.source, { skipOriginPush: true });
|
||||
return;
|
||||
}
|
||||
// target.type === 'page' — fully exit the artist-detail chain
|
||||
artistDetailPageState.currentArtistId = null;
|
||||
artistDetailPageState.currentArtistName = null;
|
||||
artistDetailPageState.originStack = [];
|
||||
navigateToPage(target.pageId);
|
||||
if (window.history.length > 1) {
|
||||
window.history.back();
|
||||
return;
|
||||
}
|
||||
|
||||
// No history — default to library
|
||||
artistDetailPageState.currentArtistId = null;
|
||||
artistDetailPageState.currentArtistName = null;
|
||||
artistDetailPageState.originStack = [];
|
||||
navigateToPage('library');
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1237,7 +1237,7 @@ function initExpandedPlayer() {
|
|||
gotoArtistBtn.addEventListener('click', () => {
|
||||
if (currentTrack && currentTrack.artist_id) {
|
||||
closeNowPlayingModal();
|
||||
navigateToArtistDetail(currentTrack.artist_id, currentTrack.artist || '');
|
||||
navigateToArtistDetailPage(currentTrack.artist_id, currentTrack.artist || '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -2396,4 +2396,3 @@ function updateMediaSessionPlaybackState() {
|
|||
}
|
||||
|
||||
// ===============================
|
||||
|
||||
|
|
|
|||
|
|
@ -320,7 +320,7 @@ function initializeSearchModeToggle() {
|
|||
onClick: () => {
|
||||
console.log(`🎵 Opening library artist detail: ${artist.name} (ID: ${artist.id})`);
|
||||
hideDropdown();
|
||||
navigateToArtistDetail(artist.id, artist.name);
|
||||
navigateToArtistDetailPage(artist.id, artist.name);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
@ -341,7 +341,7 @@ function initializeSearchModeToggle() {
|
|||
const sourceOverride = searchController.state.activeSource;
|
||||
console.log(`🎵 Opening artist detail: ${artist.name} (ID: ${artist.id}, source: ${sourceOverride})`);
|
||||
hideDropdown();
|
||||
navigateToArtistDetail(artist.id, artist.name, sourceOverride || null);
|
||||
navigateToArtistDetailPage(artist.id, artist.name, sourceOverride || null);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3862,7 +3862,7 @@ function createSimilarArtistBubble(artist) {
|
|||
// library and source artists thanks to the source-aware backend endpoint.
|
||||
bubble.addEventListener('click', () => {
|
||||
console.log(`🎵 Clicked similar artist: ${artist.name} (ID: ${artist.id})`);
|
||||
navigateToArtistDetail(artist.id, artist.name, artist.source || null);
|
||||
navigateToArtistDetailPage(artist.id, artist.name, artist.source || null);
|
||||
});
|
||||
|
||||
return bubble;
|
||||
|
|
|
|||
|
|
@ -165,6 +165,7 @@ window.SoulSyncWebShellBridge = {
|
|||
activateLegacyPath(pathname) {
|
||||
activateLegacyPath(pathname);
|
||||
},
|
||||
navigateToArtistDetailPage,
|
||||
navigateToArtistDetail,
|
||||
showReactHost(pageId) {
|
||||
showReactHost(pageId);
|
||||
|
|
|
|||
|
|
@ -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="navigateToPage('library');setTimeout(()=>navigateToArtistDetail('${item.id}','${_esc(item.name).replace(/'/g, "\\'")}'),300)">${_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" 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-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="navigateToPage('library');setTimeout(()=>navigateToArtistDetail('${item.artist_id}','${_esc(item.artist || '').replace(/'/g, "\\'")}'),300)">${_esc(item.artist || '')}</a>` : _esc(item.artist || '')}</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>
|
||||
<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="navigateToPage('library');setTimeout(()=>navigateToArtistDetail('${item.artist_id}','${_esc(item.artist || '').replace(/'/g, "\\'")}'),300)">${_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" onclick="navigateToArtistDetailPage('${item.artist_id}','${_esc(item.artist || '').replace(/'/g, "\\'")}')">${_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 ? `navigateToPage('library');setTimeout(()=>navigateToArtistDetail('${a.id}','${_esc(a.name).replace(/'/g, "\\\\'")}'),300)` : ''}" style="cursor:${a.id ? 'pointer' : 'default'}">
|
||||
return `<div class="stats-artist-bubble" onclick="${a.id ? `navigateToArtistDetailPage('${a.id}','${_esc(a.name).replace(/'/g, "\\\\'")}')` : ''}" style="cursor:${a.id ? 'pointer' : 'default'}">
|
||||
<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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue