diff --git a/webui/static/helper.js b/webui/static/helper.js index 71d57a67..e4be3c99 100644 --- a/webui/static/helper.js +++ b/webui/static/helper.js @@ -3417,6 +3417,7 @@ const WHATS_NEW = { { date: 'May 18, 2026 — 2.5.6 release' }, { title: 'MusicBrainz as Primary Metadata Source', desc: 'MusicBrainz is now a full primary metadata source on equal footing with Deezer, iTunes, Spotify, and Discogs. switch to it in Settings → Metadata Source — always available, no account or API key needed, rate-limited to 1 req/sec. covers all primary source flows: search, album/track/artist lookup, watchlist scans, discover hero, similar artist backfill, artist map.', page: 'settings' }, { title: 'Fix: MusicBrainz artist detail showing MBID as name', desc: 'clicking a MusicBrainz artist from search results was showing the raw MBID as the artist name on the detail page. URL-driven routing (PR #644) no longer passes the display name to the backend, so the source detail endpoint now looks it up directly from MusicBrainz by MBID.' }, + { title: 'Fix: artist detail back button always showing "← Back"', desc: 'PR #644 removed the back-button label logic along with the origin stack. restored: a label stack (separate from browser history, which handles actual navigation) tracks where you came from across the full similar-artist chain — "← Back to Search", "← Back to Artist A", "← Back to Artist B", etc. API response backfills the current artist name so the stack has real names when clicking similar artists.' }, ], '2.5.5': [ { date: 'May 17, 2026 — 2.5.5 release' }, diff --git a/webui/static/library.js b/webui/static/library.js index f915d73a..bce5bfbf 100644 --- a/webui/static/library.js +++ b/webui/static/library.js @@ -680,6 +680,26 @@ async function toggleLibraryCardWatchlist(btn, artist) { // =============================================== // Artist detail page state +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', +}; + +// Stack of origins for the back-button label. Each entry: {type:'page', pageId} +// or {type:'artist', name}. Pushed on forward navigation, popped on back. +// Separate from browser history — only used for the label display. +let _artistDetailLabelStack = []; +let _artistDetailGoingBack = false; + let artistDetailPageState = { isInitialized: false, currentArtistId: null, @@ -797,6 +817,23 @@ function navigateToArtistDetail(artistId, artistName, sourceOverride = null, opt } console.log(`🎵 Navigating to artist detail: ${artistName} (ID: ${artistId}${sourceOverride ? `, source: ${sourceOverride}` : ''})`); + // Maintain the label stack. Back navigations pop; forward navigations push. + if (_artistDetailGoingBack) { + _artistDetailLabelStack.pop(); + _artistDetailGoingBack = false; + } else { + if (currentPage !== 'artist-detail') { + _artistDetailLabelStack = []; // fresh chain from a non-artist page + } + if (currentPage === 'artist-detail' && artistDetailPageState.currentArtistName) { + _artistDetailLabelStack.push({ type: 'artist', name: artistDetailPageState.currentArtistName }); + } else { + const pageId = (typeof currentPage === 'string' && currentPage && currentPage !== 'artist-detail') + ? currentPage : 'library'; + _artistDetailLabelStack.push({ type: 'page', pageId }); + } + } + // Abort any in-progress completion stream if (artistDetailPageState.completionController) { artistDetailPageState.completionController.abort(); @@ -851,10 +888,28 @@ function navigateToArtistDetail(artistId, artistName, sourceOverride = null, opt initializeArtistDetailPage(); } + _updateArtistDetailBackButtonLabel(); + // Load artist data loadArtistDetailData(artistId, artistName); } +function _updateArtistDetailBackButtonLabel() { + const backBtnLabel = document.querySelector('#artist-detail-back-btn span'); + if (!backBtnLabel) return; + const top = _artistDetailLabelStack[_artistDetailLabelStack.length - 1]; + if (!top) { + backBtnLabel.textContent = '← Back'; + return; + } + 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..."); @@ -870,12 +925,15 @@ function initializeArtistDetailPage() { } if (window.history.length > 1) { + _artistDetailGoingBack = true; window.history.back(); return; } - // No history — default to library - navigateToPage('library'); + // No history — fall back to recorded origin page or library + const top = _artistDetailLabelStack.pop(); + _updateArtistDetailBackButtonLabel(); + navigateToPage(top?.type === 'page' ? (top.pageId || 'library') : 'library'); }); } @@ -981,6 +1039,15 @@ async function loadArtistDetailData(artistId, artistName) { artistDetailPageState.currentArtistId = data.artist.id; } + // Backfill name from API response — URL-driven navigation passes '' for the + // name so the label stack has real names when the user clicks a similar artist. + if (data.artist?.name && !artistDetailPageState.currentArtistName) { + artistDetailPageState.currentArtistName = data.artist.name; + if (typeof _updateSidebarLibraryBreadcrumb === 'function') { + _updateSidebarLibraryBreadcrumb(); + } + } + // Keep the resolved metadata source for album-track lookups. artistDetailPageState.currentArtistSource = data.discography?.source || data.artist?.source || null;