Route all artist-detail callers to the standalone page, bump to 2.45

Phase 4a of the Search/Artists unification. The app had two artist-
detail implementations: the standalone page Library navigates to via
navigateToArtistDetail (its own route, deep-link support, highlights
Library in the sidebar), and an inline state inside the Artists page
reached via selectArtistForDetail. They rendered similar content but
were separate code paths and kept drifting apart (PR #356 just had
to fix source propagation in both).

Every external caller of selectArtistForDetail (9 sites across
api-monitor.js, discover.js, downloads.js, search.js) now calls
navigateToArtistDetail(id, name, source) directly. Removed ~63 lines
of the navigate-then-setTimeout-then-select dance. Source context
(Spotify/iTunes/Deezer/etc.) carries cleanly through via the new
third argument.

Artists sidebar entry, its inline search, and selectArtistForDetail
all still work — they just have no external callers. Phase 4b will
retire the sidebar entry and artists.js.
This commit is contained in:
Broque Thomas 2026-04-22 13:36:36 -07:00
parent f203b3e46d
commit 9361c29965
7 changed files with 33 additions and 80 deletions

View file

@ -37,7 +37,7 @@ _log_dir = Path(_log_path).parent
logger = setup_logging(_log_level, _log_path)
# App version — single source of truth for backup metadata, version-info endpoint, etc.
_SOULSYNC_BASE_VERSION = "2.44"
_SOULSYNC_BASE_VERSION = "2.45"
def _build_version_string():
"""Append short commit hash to version when available (e.g. 2.35+abc1234)."""
@ -22809,6 +22809,17 @@ def get_version_info():
"title": "What's New in SoulSync",
"subtitle": f"Version {SOULSYNC_VERSION} — Latest Changes",
"sections": [
{
"title": "Artist Links Everywhere Go to the Same Page",
"description": "Clicking an artist result in Search, Discover, the API Monitor, or anywhere else now lands on the standalone artist detail page Library already uses — instead of swapping into the Artists page's inline detail view",
"features": [
"• One artist detail page, not two — less navigation surprise and a stable URL (/artist-detail) for deep links",
"• Source context (Spotify/iTunes/Deezer/etc.) now carries cleanly into the destination so non-Spotify albums load from the right provider",
"• Removed the navigate-then-setTimeout dance at 9 callsites (api-monitor, discover, downloads, search, library recommendations)",
"• Artists sidebar entry and its inline search still work for now — next phase retires that page entirely",
"• Phase 4a of the Search/Artists unification project",
],
},
{
"title": "Remove Embedded Download Manager from Search Page",
"description": "The Search page used to carry a second copy of the Download Manager (active + finished queues, clear/cancel-all buttons) that was hidden by default and duplicated the dedicated Downloads page. That duplicate is gone",

View file

@ -2382,16 +2382,9 @@ async function openWatchlistArtistDetailView(artistId, artistName) {
source = spotify_artist_id ? 'spotify' : discogs_artist_id ? 'discogs' : deezer_artist_id ? 'deezer' : 'itunes';
}
if (discogId) {
// Close detail overlay and navigate to Artists page
// Close detail overlay and navigate to the standalone artist detail page
closeWatchlistArtistDetailView();
// Navigate to Artists page and load discography
navigateToPage('artists');
setTimeout(() => {
selectArtistForDetail(
{ id: discogId, name: artistName, image_url: artist.image_url || '' },
{ source: source }
);
}, 200);
navigateToArtistDetail(discogId, artistName, source);
}
});

View file

@ -739,16 +739,7 @@ async function checkRecommendedWatchlistStatuses(artists) {
async function viewRecommendedArtistDiscography(artistId, artistName) {
closeRecommendedArtistsModal();
const artist = {
id: artistId,
name: artistName
};
// Use same navigation pattern as hero slider
navigateToPage('artists');
await new Promise(resolve => setTimeout(resolve, 100));
await selectArtistForDetail(artist);
navigateToArtistDetail(artistId, artistName);
}
async function checkAllHeroWatchlistStatus() {
@ -830,25 +821,8 @@ async function viewDiscoverHeroDiscography() {
return;
}
// Create artist object matching the expected format
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}`);
// Navigate to Artists page
navigateToPage('artists');
// Small delay to let the page load
await new Promise(resolve => setTimeout(resolve, 100));
// Load the artist details
await selectArtistForDetail(artist);
navigateToArtistDetail(artistId, artistName);
}
function showDiscoverHeroEmpty() {
@ -4639,9 +4613,9 @@ function _renderYourArtistCard(artist) {
const watchlistClass = artist.on_watchlist ? 'active' : '';
const hasId = artist.active_source_id && artist.active_source_id !== '';
// Navigate to artist page (name click)
// Navigate to standalone artist detail page (name click)
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
@ -4820,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>
@ -6720,7 +6694,7 @@ function _artMapSetupInteraction(canvas) {
<div class="artmap-ctx-item" onclick="_artMapHideContextMenu(); ${hasId ? `openYourArtistInfoModal_direct(${JSON.stringify(node).replace(/"/g, '&quot;')})` : ''}">
<span>&#9432;</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>&#128191;</span> View Discography
</div>
<div class="artmap-ctx-item" onclick="_artMapHideContextMenu(); toggleYourArtistWatchlist(0,'${escapeForInlineJs(node.name)}','${escapeForInlineJs(bestId)}','${bestSource}',null)">
@ -7408,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)}')` : ''}">

View file

@ -634,16 +634,7 @@ function _navigateToArtistFromModal(artistId, artistName, imageUrl, source, play
if (!artistName) return;
// Close the download modal
if (playlistId) closeDownloadMissingModal(playlistId);
// Navigate to Artists page and load discography
navigateToPage('artists');
setTimeout(() => {
// If we have an artist ID, use it directly
// If not, search by name — selectArtistForDetail handles both
selectArtistForDetail(
{ id: artistId || artistName, name: artistName, image_url: imageUrl || '' },
source ? { source: source } : undefined
);
}, 200);
navigateToArtistDetail(artistId || artistName, artistName, source || null);
}
async function closeDownloadMissingModal(playlistId) {
@ -5434,18 +5425,8 @@ function _gsSwitchSource(src) {
function _gsClickArtist(id, name, isLibrary) {
_gsDeactivate();
if (isLibrary) {
// Same as enhanced search: navigateToArtistDetail
navigateToArtistDetail(id, name);
} else {
// Same as enhanced search: navigate to Artists page + selectArtistForDetail
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) {

View file

@ -3599,6 +3599,11 @@ function closeHelperSearch() {
// ═══════════════════════════════════════════════════════════════════════════
const WHATS_NEW = {
'2.45': [
// --- April 23, 2026 (night) ---
{ date: 'April 23, 2026 (night)' },
{ title: 'Artist Links Everywhere Go to the Same Page', desc: 'Clicking an artist result in Search, Discover, the API Monitor, or anywhere else now lands on the standalone artist detail page that Library already uses — instead of swapping into the Artists page\'s inline detail view. One artist detail page, not two: less navigation surprise, a stable /artist-detail URL for deep links, and source context (Spotify/iTunes/Deezer/etc.) now carries cleanly into the destination so non-Spotify albums load from the right provider. Removed the navigate-then-setTimeout dance at 9 callsites. Artists sidebar entry and its inline search still work for now — next phase retires that page entirely. Phase 4a of the Search/Artists unification project', page: 'library' },
],
'2.44': [
// --- April 23, 2026 (evening) ---
{ date: 'April 23, 2026 (evening)' },

View file

@ -655,8 +655,8 @@ let discographyFilterState = {
ownership: 'all' // 'all', 'owned', 'missing'
};
function navigateToArtistDetail(artistId, artistName) {
console.log(`🎵 Navigating to artist detail: ${artistName} (ID: ${artistId})`);
function navigateToArtistDetail(artistId, artistName, sourceOverride = null) {
console.log(`🎵 Navigating to artist detail: ${artistName} (ID: ${artistId}${sourceOverride ? `, source: ${sourceOverride}` : ''})`);
// Abort any in-progress completion stream
if (artistDetailPageState.completionController) {
@ -672,7 +672,7 @@ function navigateToArtistDetail(artistId, artistName) {
// Store current artist info and reset enhanced view state
artistDetailPageState.currentArtistId = artistId;
artistDetailPageState.currentArtistName = artistName;
artistDetailPageState.currentArtistSource = null;
artistDetailPageState.currentArtistSource = sourceOverride || null;
artistDetailPageState.enhancedData = null;
artistDetailPageState.expandedAlbums = new Set();
artistDetailPageState.selectedTracks = new Set();

View file

@ -340,18 +340,7 @@ function initializeSearchModeToggle() {
const sourceOverride = _activeSearchSource;
console.log(`🎵 Opening artist detail: ${artist.name} (ID: ${artist.id}, source: ${sourceOverride})`);
hideDropdown();
// Navigate to Artists page
navigateToPage('artists');
// Small delay to let the page load
await new Promise(resolve => setTimeout(resolve, 100));
// Load the artist details with source context
await selectArtistForDetail(artist, {
source: sourceOverride,
plugin: artist.external_urls?.hydrabase_plugin,
});
navigateToArtistDetail(artist.id, artist.name, sourceOverride || null);
}
})
);