Unify artist detail: route source artists to standalone page, retire inline Artists page
Completes the artist-detail unification. Source artists now land on the same /artist-detail page as library artists (with the source-aware backend endpoint from earlier this session handling the data fetch). The inline Artists page is gone — artists.js deleted, #artists-page HTML block removed, /artists URL aliases to /search. Source-artist callsites re-migrated from selectArtistForDetail to navigateToArtistDetail (search results, global widget, download modal, Discover hero / Your Artists cards / artmap context / genre deep-dive, watchlist artist detail). Visual upgrade to standalone hero: added .artist-detail-hero-bg + .artist-detail-hero-overlay (blurred image bg, dark gradient — same treatment as the inline page). library.js sets the bg image when loading an artist. Library-only UI hidden via CSS for source artists (existing rules from the previous commit cover Enhanced toggle, Status filter, completion bars, enrichment coverage, Top Tracks sidebar, Radio / Enhance buttons). Final 2 helpers (lazyLoadArtistImages used by wishlist-tools, showCompletionError used by completion checker) moved from artists.js into shared-helpers.js. The inline-page candidate set was dropped from _resolveSimilarArtistsTargets. init.js: 'artists' alias added at top of navigateToPage (same pattern as the existing 'downloads' alias). 'case artists:' handler removed from loadPageData. _getPageFromPath now maps artist-detail to library as its parent (matches the existing nav highlight at init.js:2161). tests/test_script_split_integrity.py: artists.js removed from SPLIT_MODULES; KNOWN_CROSS_FILE_DUPES updated to point escapeHtml at shared-helpers.js instead of artists.js. 354/354 tests pass. Net delta: -1700 lines. Stays at 2.39. Once you've verified end-to-end (library artist -> hero looks like inline visual; source artist from Search -> same page, similar artists works, no 404s; /artists URL -> /search), a follow-up commit bumps to 2.40 with the full WHATS_NEW entry that's already prepped.
This commit is contained in:
parent
5219780c01
commit
93f1941829
11 changed files with 153 additions and 1854 deletions
|
|
@ -39,7 +39,6 @@ SPLIT_MODULES = [
|
|||
"downloads.js",
|
||||
"wishlist-tools.js",
|
||||
"sync-services.js",
|
||||
"artists.js",
|
||||
"api-monitor.js",
|
||||
"library.js",
|
||||
"beatport-ui.js",
|
||||
|
|
@ -57,7 +56,7 @@ NON_SPLIT_JS = {"setup-wizard.js", "docs.js", "helper.js", "particles.js", "work
|
|||
# In a plain <script> context the last-loaded declaration wins. These are NOT
|
||||
# regressions from the split — they should be deduplicated in a follow-up.
|
||||
KNOWN_CROSS_FILE_DUPES = {
|
||||
"escapeHtml", # downloads.js, artists.js, discover.js
|
||||
"escapeHtml", # downloads.js, shared-helpers.js, discover.js
|
||||
"formatDuration", # sync-spotify.js, wishlist-tools.js, sync-services.js
|
||||
"matchedDownloadTrack", # downloads.js, wishlist-tools.js
|
||||
"matchedDownloadAlbum", # downloads.js, wishlist-tools.js
|
||||
|
|
|
|||
145
webui/index.html
145
webui/index.html
|
|
@ -2100,146 +2100,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Artists Page -->
|
||||
<div class="page" id="artists-page">
|
||||
<!-- Initial Search State -->
|
||||
<div class="artists-search-state" id="artists-search-state">
|
||||
<div class="artists-search-container">
|
||||
<div class="artists-welcome-section">
|
||||
<h2 class="artists-welcome-title"><img src="/static/discover.png" class="page-header-icon" alt=""><span>Discover Artists</span></h2>
|
||||
<p class="artists-welcome-subtitle">Search for your favorite artists and explore their
|
||||
complete discography</p>
|
||||
</div>
|
||||
|
||||
<div class="artists-search-input-container">
|
||||
<input type="text" id="artists-search-input" class="artists-search-input"
|
||||
placeholder="Search for an artist...">
|
||||
<div class="artists-search-icon">🔍</div>
|
||||
</div>
|
||||
|
||||
<div class="artists-search-status" id="artists-search-status">
|
||||
Start typing to search for artists
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Results State -->
|
||||
<div class="artists-results-state hidden" id="artists-results-state">
|
||||
<div class="artists-results-header">
|
||||
<button class="artists-back-button" id="artists-back-button">
|
||||
<span class="back-icon">←</span>
|
||||
<span>Back to Search</span>
|
||||
</button>
|
||||
|
||||
<div class="artists-search-header">
|
||||
<input type="text" id="artists-header-search-input" class="artists-header-search-input"
|
||||
placeholder="Search for an artist...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="artists-results-content">
|
||||
<div class="artists-results-title">Search Results</div>
|
||||
<div class="artists-cards-container" id="artists-cards-container">
|
||||
<!-- Artist cards will be dynamically populated here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Artist Detail State -->
|
||||
<div class="artist-detail-state hidden" id="artist-detail-state">
|
||||
<!-- Hero Section -->
|
||||
<div class="artists-hero-section" id="artists-hero-section">
|
||||
<div class="artists-hero-bg" id="artists-hero-bg"></div>
|
||||
<div class="artists-hero-overlay"></div>
|
||||
<div class="artists-hero-content">
|
||||
<button class="artists-hero-back" id="artist-detail-back-button">
|
||||
<span>←</span> Back
|
||||
</button>
|
||||
<div class="artists-hero-main">
|
||||
<div class="artists-hero-image" id="artists-hero-image"></div>
|
||||
<div class="artists-hero-info">
|
||||
<h1 class="artists-hero-name" id="artists-hero-name">Artist Name</h1>
|
||||
<div class="artists-hero-badges" id="artists-hero-badges"></div>
|
||||
<div class="artists-hero-genres" id="artists-hero-genres"></div>
|
||||
<div class="artists-hero-bio" id="artists-hero-bio"></div>
|
||||
<div class="artists-hero-stats" id="artists-hero-stats"></div>
|
||||
<button class="discog-download-btn discog-btn-compact" id="discog-download-btn-artists" onclick="openDiscographyModal()" style="display:none;">
|
||||
<span class="discog-btn-icon">⬇</span>
|
||||
<span class="discog-btn-text">Download Discography</span>
|
||||
<span class="discog-btn-shimmer"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="artists-hero-actions">
|
||||
<button class="artist-detail-watchlist-btn" id="artist-detail-watchlist-btn">
|
||||
<span class="watchlist-icon">👁️</span>
|
||||
<span class="watchlist-text">Add to Watchlist</span>
|
||||
</button>
|
||||
<button class="artist-detail-watchlist-settings-btn hidden" id="artist-detail-watchlist-settings-btn" title="Watchlist Settings">
|
||||
⚙
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Keep old hidden elements for backward compatibility with JS refs -->
|
||||
<div id="search-artist-detail-image" style="display:none"></div>
|
||||
<div id="search-artist-detail-name" style="display:none"></div>
|
||||
<div id="search-artist-detail-genres" style="display:none"></div>
|
||||
|
||||
<div class="artist-detail-content">
|
||||
<div class="artist-detail-tabs">
|
||||
<button class="artist-tab active" data-tab="albums" id="albums-tab">
|
||||
<span class="tab-icon">💿</span>
|
||||
<span>Albums</span>
|
||||
</button>
|
||||
<button class="artist-tab" data-tab="singles" id="singles-tab">
|
||||
<span class="tab-icon">🎵</span>
|
||||
<span>Singles & EPs</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="artist-detail-discography">
|
||||
<div class="tab-content active" id="albums-content">
|
||||
<div class="album-cards-container" id="album-cards-container">
|
||||
<!-- Album cards will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="singles-content">
|
||||
<div class="singles-cards-container" id="singles-cards-container">
|
||||
<!-- Singles cards will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Similar Artists Section -->
|
||||
<div class="similar-artists-section" id="similar-artists-section">
|
||||
<div class="similar-artists-header">
|
||||
<h3 class="similar-artists-title">Similar Artists</h3>
|
||||
<p class="similar-artists-subtitle">Discover artists with a similar sound</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div class="similar-artists-loading hidden" id="similar-artists-loading">
|
||||
<div class="loading-spinner-small"></div>
|
||||
<span>Finding similar artists...</span>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div class="similar-artists-error hidden" id="similar-artists-error">
|
||||
<span class="error-icon">⚠️</span>
|
||||
<span class="error-text">Unable to load similar artists</span>
|
||||
</div>
|
||||
|
||||
<!-- Similar Artists Bubbles Container -->
|
||||
<div class="similar-artists-bubbles-container" id="similar-artists-bubbles-container">
|
||||
<!-- Artist bubble cards will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Automations Page -->
|
||||
<div class="page" id="automations-page">
|
||||
|
|
@ -2497,6 +2357,10 @@
|
|||
|
||||
<!-- Artist Hero Section -->
|
||||
<div class="artist-hero-section" id="artist-hero-section">
|
||||
<!-- Blurred background image + dark overlay (inline-Artists hero treatment) -->
|
||||
<div class="artist-detail-hero-bg" id="artist-detail-hero-bg"></div>
|
||||
<div class="artist-detail-hero-overlay"></div>
|
||||
|
||||
<div class="artist-hero-content">
|
||||
<!-- Left: Image -->
|
||||
<div class="artist-image-container">
|
||||
|
|
@ -8017,7 +7881,6 @@
|
|||
<script src="{{ url_for('static', filename='downloads.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='wishlist-tools.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='sync-services.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='artists.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='api-monitor.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='library.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='beatport-ui.js') }}"></script>
|
||||
|
|
|
|||
|
|
@ -2385,16 +2385,8 @@ async function openWatchlistArtistDetailView(artistId, artistName) {
|
|||
source = spotify_artist_id ? 'spotify' : discogs_artist_id ? 'discogs' : deezer_artist_id ? 'deezer' : 'itunes';
|
||||
}
|
||||
if (discogId) {
|
||||
// Watchlist discogId is a metadata-source id (Spotify/Deezer/iTunes),
|
||||
// not a library PK — route through the Artists page inline view.
|
||||
closeWatchlistArtistDetailView();
|
||||
navigateToPage('artists');
|
||||
setTimeout(() => {
|
||||
selectArtistForDetail(
|
||||
{ id: discogId, name: artistName, image_url: artist.image_url || '' },
|
||||
{ source: source }
|
||||
);
|
||||
}, 200);
|
||||
navigateToArtistDetail(discogId, artistName, source);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -739,14 +739,7 @@ async function checkRecommendedWatchlistStatuses(artists) {
|
|||
|
||||
async function viewRecommendedArtistDiscography(artistId, artistName) {
|
||||
closeRecommendedArtistsModal();
|
||||
|
||||
const artist = { id: artistId, name: artistName };
|
||||
|
||||
// Recommended artists come from the metadata source — route through the
|
||||
// Artists page's inline view so the source-provided id resolves correctly.
|
||||
navigateToPage('artists');
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
await selectArtistForDetail(artist);
|
||||
navigateToArtistDetail(artistId, artistName);
|
||||
}
|
||||
|
||||
async function checkAllHeroWatchlistStatus() {
|
||||
|
|
@ -828,21 +821,8 @@ async function viewDiscoverHeroDiscography() {
|
|||
return;
|
||||
}
|
||||
|
||||
const artist = {
|
||||
id: artistId,
|
||||
name: artistName,
|
||||
image_url: discoverHeroArtists[discoverHeroIndex]?.image_url || '',
|
||||
genres: discoverHeroArtists[discoverHeroIndex]?.genres || [],
|
||||
popularity: discoverHeroArtists[discoverHeroIndex]?.popularity || 0
|
||||
};
|
||||
|
||||
console.log(`🎵 Navigating to artist detail for: ${artistName}`);
|
||||
|
||||
// Hero artists are source-provided recommendations — route through the
|
||||
// Artists page's inline view so the source id resolves correctly.
|
||||
navigateToPage('artists');
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
await selectArtistForDetail(artist);
|
||||
navigateToArtistDetail(artistId, artistName);
|
||||
}
|
||||
|
||||
function showDiscoverHeroEmpty() {
|
||||
|
|
@ -4635,7 +4615,7 @@ function _renderYourArtistCard(artist) {
|
|||
|
||||
// Navigate to Artists page (name click) — source artist id, needs inline view
|
||||
const navAction = hasId
|
||||
? `event.stopPropagation(); navigateToPage('artists'); setTimeout(() => selectArtistForDetail({id:'${escapeForInlineJs(artist.active_source_id)}', name:'${escapeForInlineJs(artist.artist_name)}', image_url:'${escapeForInlineJs(img)}'}), 200)`
|
||||
? `event.stopPropagation(); navigateToArtistDetail('${escapeForInlineJs(artist.active_source_id)}', '${escapeForInlineJs(artist.artist_name)}')`
|
||||
: '';
|
||||
|
||||
// Open info modal (card body click) — pass pool ID so we can look up all data
|
||||
|
|
@ -4814,7 +4794,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(); navigateToPage('artists'); setTimeout(() => selectArtistForDetail({id:'${escapeForInlineJs(artistId)}', name:'${escapeForInlineJs(artistName)}', image_url:'${escapeForInlineJs(imageUrl)}'}, {source:'${escapeForInlineJs(pool.active_source || '')}'}), 200)">
|
||||
<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)">
|
||||
<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>
|
||||
|
|
@ -6714,7 +6694,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(); navigateToPage('artists'); setTimeout(() => selectArtistForDetail({id:'${escapeForInlineJs(bestId)}',name:'${escapeForInlineJs(node.name)}',image_url:'${escapeForInlineJs(node.image_url || '')}'},{source:'${bestSource}'}), 200)">
|
||||
<div class="artmap-ctx-item" onclick="_artMapHideContextMenu(); navigateToArtistDetail('${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)">
|
||||
|
|
@ -7402,7 +7382,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();navigateToPage('artists');setTimeout(()=>selectArtistForDetail({id:'${_esc(a.entity_id)}',name:'${_esc(a.name)}',image_url:'${imgUrl}'},{source:'${artSource}'}),300)"`;
|
||||
const clickAction = `onclick="document.getElementById('genre-deep-dive-modal').remove();navigateToArtistDetail('${_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)}')` : ''}">
|
||||
|
|
|
|||
|
|
@ -634,15 +634,7 @@ function _navigateToArtistFromModal(artistId, artistName, imageUrl, source, play
|
|||
if (!artistName) return;
|
||||
// Close the download modal
|
||||
if (playlistId) closeDownloadMissingModal(playlistId);
|
||||
// The id from a download modal is typically a metadata-source id; route via
|
||||
// the Artists page inline view so the source-aware discography endpoint runs.
|
||||
navigateToPage('artists');
|
||||
setTimeout(() => {
|
||||
selectArtistForDetail(
|
||||
{ id: artistId || artistName, name: artistName, image_url: imageUrl || '' },
|
||||
source ? { source: source } : undefined
|
||||
);
|
||||
}, 200);
|
||||
navigateToArtistDetail(artistId || artistName, artistName, source || null);
|
||||
}
|
||||
|
||||
async function closeDownloadMissingModal(playlistId) {
|
||||
|
|
@ -5433,19 +5425,8 @@ function _gsSwitchSource(src) {
|
|||
|
||||
function _gsClickArtist(id, name, isLibrary) {
|
||||
_gsDeactivate();
|
||||
if (isLibrary) {
|
||||
// Library artists: id is a local DB PK — use the standalone artist-detail page.
|
||||
navigateToArtistDetail(id, name);
|
||||
} else {
|
||||
// Source artists: id is a Deezer/Spotify/iTunes id — route to the Artists
|
||||
// page's inline view which fetches discography from the source.
|
||||
navigateToPage('artists');
|
||||
setTimeout(() => {
|
||||
selectArtistForDetail({ id, name, image_url: '' }, {
|
||||
source: _gsState.activeSource || '',
|
||||
});
|
||||
}, 150);
|
||||
}
|
||||
const source = isLibrary ? null : (_gsState.activeSource || null);
|
||||
navigateToArtistDetail(id, name, source);
|
||||
}
|
||||
|
||||
async function _gsClickAlbum(albumId, albumName, artistName, imageUrl, source) {
|
||||
|
|
|
|||
|
|
@ -2010,7 +2010,7 @@ function _getPageFromPath() {
|
|||
const basePage = path.split('/')[0];
|
||||
if (!_DEEPLINK_VALID_PAGES.has(basePage)) return 'dashboard';
|
||||
// Context-dependent pages fall back to a sensible parent
|
||||
if (basePage === 'artist-detail') return 'artists';
|
||||
if (basePage === 'artist-detail') return 'library';
|
||||
if (basePage === 'playlist-explorer') return 'library';
|
||||
return basePage;
|
||||
}
|
||||
|
|
@ -2115,8 +2115,10 @@ function initializeWatchlist() {
|
|||
}
|
||||
|
||||
function navigateToPage(pageId, options = {}) {
|
||||
// Backwards-compat alias — the Search page used to live under id 'downloads'.
|
||||
if (pageId === 'downloads') pageId = 'search';
|
||||
// Backwards-compat aliases — both legacy ids fold into the unified Search page.
|
||||
// 'downloads' was the Search page's old id; 'artists' was the retired inline
|
||||
// Artists page, now replaced by clicking artists from the unified Search.
|
||||
if (pageId === 'downloads' || pageId === 'artists') pageId = 'search';
|
||||
|
||||
if (pageId === currentPage) return;
|
||||
|
||||
|
|
@ -2218,15 +2220,7 @@ async function loadPageData(pageId) {
|
|||
initializeSearchModeToggle();
|
||||
initializeFilters();
|
||||
break;
|
||||
case 'artists':
|
||||
// Only fully initialize if not already initialized
|
||||
if (!artistsPageState.isInitialized) {
|
||||
initializeArtistsPage();
|
||||
} else {
|
||||
// Just restore state if already initialized
|
||||
restoreArtistsPageState();
|
||||
}
|
||||
break;
|
||||
// 'artists' page retired — aliased to 'search' at the top of navigateToPage
|
||||
case 'active-downloads':
|
||||
loadActiveDownloadsPage();
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -1063,6 +1063,17 @@ function updateArtistHeaderStats(albumCount, trackCount) {
|
|||
function updateArtistHeroSection(artist, discography) {
|
||||
console.log("🖼️ Updating artist hero section");
|
||||
|
||||
// Blurred background image (inline-Artists hero treatment) — set whenever
|
||||
// we have an image_url; falls back to clearing the bg if not.
|
||||
const heroBg = document.getElementById("artist-detail-hero-bg");
|
||||
if (heroBg) {
|
||||
if (artist.image_url && artist.image_url.trim() !== "" && artist.image_url !== "null") {
|
||||
heroBg.style.backgroundImage = `url('${artist.image_url}')`;
|
||||
} else {
|
||||
heroBg.style.backgroundImage = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Update artist image with detailed debugging
|
||||
const imageElement = document.getElementById("artist-detail-image");
|
||||
const fallbackElement = document.getElementById("artist-detail-image-fallback");
|
||||
|
|
|
|||
|
|
@ -336,21 +336,11 @@ function initializeSearchModeToggle() {
|
|||
name: artist.name,
|
||||
meta: 'Artist',
|
||||
badge: sourceBadge,
|
||||
onClick: async () => {
|
||||
onClick: () => {
|
||||
const sourceOverride = _activeSearchSource;
|
||||
console.log(`🎵 Opening artist detail: ${artist.name} (ID: ${artist.id}, source: ${sourceOverride})`);
|
||||
hideDropdown();
|
||||
|
||||
// Source artists are NOT library entries — their id is a Deezer/
|
||||
// Spotify/iTunes id, not a library PK. Route to the Artists page's
|
||||
// inline selectArtistForDetail which fetches discography from the
|
||||
// source directly, not the library's /api/artist-detail endpoint.
|
||||
navigateToPage('artists');
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
await selectArtistForDetail(artist, {
|
||||
source: sourceOverride,
|
||||
plugin: artist.external_urls?.hydrabase_plugin,
|
||||
});
|
||||
navigateToArtistDetail(artist.id, artist.name, sourceOverride || null);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2768,41 +2768,19 @@ function renderEnrichmentCards(enrichment) {
|
|||
// container that live on the artist-detail page.
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Similar artists lives on two pages (the inline Artists page and the standalone
|
||||
// artist-detail page), each with its own set of IDs so they don't collide in the
|
||||
// DOM. This resolver picks the set whose `.page` ancestor is currently active.
|
||||
// Similar artists section lives on the standalone artist-detail page with the
|
||||
// 'ad-' prefixed ids. The resolver shape was originally designed for both the
|
||||
// inline Artists page and the standalone page; the inline page has since been
|
||||
// retired, so only the standalone candidate remains.
|
||||
function _resolveSimilarArtistsTargets() {
|
||||
const candidates = [
|
||||
// standalone artist-detail page (scoped ids)
|
||||
{ section: 'ad-similar-artists-section', loading: 'ad-similar-artists-loading', error: 'ad-similar-artists-error', bubbles: 'ad-similar-artists-bubbles-container' },
|
||||
// legacy inline Artists page (base ids)
|
||||
{ section: 'similar-artists-section', loading: 'similar-artists-loading', error: 'similar-artists-error', bubbles: 'similar-artists-bubbles-container' },
|
||||
];
|
||||
// Prefer the set whose parent .page is currently active.
|
||||
for (const c of candidates) {
|
||||
const el = document.getElementById(c.section);
|
||||
if (el && el.closest('.page.active')) {
|
||||
return {
|
||||
section: el,
|
||||
loadingEl: document.getElementById(c.loading),
|
||||
errorEl: document.getElementById(c.error),
|
||||
container: document.getElementById(c.bubbles),
|
||||
};
|
||||
}
|
||||
}
|
||||
// Fallback: return the first set that exists at all.
|
||||
for (const c of candidates) {
|
||||
const el = document.getElementById(c.section);
|
||||
if (el) {
|
||||
return {
|
||||
section: el,
|
||||
loadingEl: document.getElementById(c.loading),
|
||||
errorEl: document.getElementById(c.error),
|
||||
container: document.getElementById(c.bubbles),
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
const sectionEl = document.getElementById('ad-similar-artists-section');
|
||||
if (!sectionEl) return null;
|
||||
return {
|
||||
section: sectionEl,
|
||||
loadingEl: document.getElementById('ad-similar-artists-loading'),
|
||||
errorEl: document.getElementById('ad-similar-artists-error'),
|
||||
container: document.getElementById('ad-similar-artists-bubbles-container'),
|
||||
};
|
||||
}
|
||||
|
||||
async function loadSimilarArtists(artistName) {
|
||||
|
|
@ -3110,21 +3088,78 @@ function createSimilarArtistBubble(artist) {
|
|||
bubble.appendChild(genres);
|
||||
}
|
||||
|
||||
// Add click handler — page-aware destination. From the standalone artist-
|
||||
// detail page, navigate to the standalone route. From the inline Artists
|
||||
// page, swap the inline view via selectArtistForDetail.
|
||||
// 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})`);
|
||||
const onStandalone = !!document.querySelector('#artist-detail-page.active');
|
||||
if (onStandalone && typeof navigateToArtistDetail === 'function') {
|
||||
navigateToArtistDetail(artist.id, artist.name, artist.source || null);
|
||||
} else if (typeof selectArtistForDetail === 'function') {
|
||||
selectArtistForDetail(
|
||||
artist,
|
||||
artist.source ? { source: artist.source, plugin: artist.plugin } : {}
|
||||
);
|
||||
}
|
||||
navigateToArtistDetail(artist.id, artist.name, artist.source || null);
|
||||
});
|
||||
|
||||
return bubble;
|
||||
}
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Lazy artist-card image loader (used by wishlist-tools.js + the legacy inline
|
||||
// Artists page search results). Fetches /api/artist/<id>/image for each card
|
||||
// flagged data-needs-image="true" in batches of 5.
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
async function lazyLoadArtistImages(container) {
|
||||
if (!container) {
|
||||
console.error('❌ lazyLoadArtistImages: container is null');
|
||||
return;
|
||||
}
|
||||
|
||||
const cardsNeedingImages = container.querySelectorAll('[data-needs-image="true"]');
|
||||
if (cardsNeedingImages.length === 0) return;
|
||||
|
||||
const batchSize = 5;
|
||||
const cards = Array.from(cardsNeedingImages);
|
||||
|
||||
for (let i = 0; i < cards.length; i += batchSize) {
|
||||
const batch = cards.slice(i, i + batchSize);
|
||||
await Promise.all(batch.map(async (card) => {
|
||||
const artistId = card.dataset.artistId;
|
||||
if (!artistId) return;
|
||||
try {
|
||||
const response = await fetch(`/api/artist/${artistId}/image`);
|
||||
const data = await response.json();
|
||||
if (data.success && data.image_url) {
|
||||
if (card.classList.contains('suggestion-card')) {
|
||||
card.style.backgroundImage = `url(${data.image_url})`;
|
||||
card.style.backgroundSize = 'cover';
|
||||
card.style.backgroundPosition = 'center';
|
||||
} else if (card.classList.contains('artist-card')) {
|
||||
const bgElement = card.querySelector('.artist-card-background');
|
||||
if (bgElement) {
|
||||
bgElement.style.cssText = `background-image: url('${data.image_url}'); background-size: cover; background-position: center;`;
|
||||
}
|
||||
}
|
||||
card.dataset.needsImage = 'false';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to load image for artist ${artistId}:`, error);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy global alias — wishlist-tools.js falls back to window.lazyLoadArtistImages
|
||||
window.lazyLoadArtistImages = lazyLoadArtistImages;
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Album-card completion overlay error state (called from checkDiscographyCompletion
|
||||
// when the API request fails)
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function showCompletionError() {
|
||||
const allOverlays = document.querySelectorAll('.completion-overlay.checking');
|
||||
allOverlays.forEach(overlay => {
|
||||
overlay.classList.remove('checking');
|
||||
overlay.classList.add('error');
|
||||
overlay.innerHTML = '<span class="completion-status">Error</span>';
|
||||
overlay.title = 'Failed to check completion status';
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59413,3 +59413,41 @@ body[data-artist-source="source"] #artist-detail-page #library-artist-enhance-bt
|
|||
body[data-artist-source="source"] #artist-detail-page .section-stats {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
/* =========================================================================
|
||||
Standalone /artist-detail page hero — blurred-background treatment
|
||||
========================================================================= */
|
||||
|
||||
#artist-detail-page .artist-hero-section {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#artist-detail-page .artist-detail-hero-bg {
|
||||
position: absolute;
|
||||
inset: -20px;
|
||||
background-size: cover;
|
||||
background-position: center top;
|
||||
background-repeat: no-repeat;
|
||||
filter: blur(50px) brightness(0.35) saturate(1.4);
|
||||
transform: scale(1.3);
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#artist-detail-page .artist-detail-hero-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg,
|
||||
rgba(8, 8, 8, 0.5) 0%,
|
||||
rgba(12, 12, 12, 0.7) 60%,
|
||||
rgba(12, 12, 12, 0.9) 100%);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#artist-detail-page .artist-hero-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue