diff --git a/webui/static/core.js b/webui/static/core.js index 3852fda7..13285531 100644 --- a/webui/static/core.js +++ b/webui/static/core.js @@ -1,5 +1,7 @@ // SoulSync WebUI JavaScript - Replicating PyQt6 GUI Functionality +const PAGE_WILL_CHANGE_EVENT = 'ss:webui-page-will-change'; + // Global state management let currentPage = 'dashboard'; let currentTrack = null; diff --git a/webui/static/init.js b/webui/static/init.js index d5ff25f4..50923744 100644 --- a/webui/static/init.js +++ b/webui/static/init.js @@ -1,5 +1,20 @@ // INITIALIZATION // =============================== +let navigationEpoch = 0; + +function notifyPageWillChange(nextPageId) { + const fromPageId = typeof currentPage === 'string' ? currentPage : null; + if (fromPageId === nextPageId) return; + + window.dispatchEvent( + new CustomEvent(PAGE_WILL_CHANGE_EVENT, { + detail: { + fromPageId, + toPageId: nextPageId, + }, + }), + ); +} // ---- Accent Color System ---- @@ -2309,6 +2324,8 @@ function initializeWatchlist() { } function navigateToPage(pageId, options = {}) { + navigationEpoch += 1; + if (!options.forceReload && pageId === currentPage) return; // Permission guard — redirect to home page if not allowed @@ -2322,11 +2339,25 @@ function navigateToPage(pageId, options = {}) { const router = getWebRouter(); if (router && !options.skipRouteChange) { + notifyPageWillChange(pageId); + const route = router.routeManifest?.find((entry) => entry.pageId === pageId); + if (route?.kind === 'react') { + showReactHost(pageId); + setActivePageChrome(pageId); + } return router.navigateToPage(pageId, { replace: options.replace === true }); } // Fallback path for initial bootstrap or environments without TanStack routing. - activatePage(pageId, { forceReload: options.forceReload === true }); + const route = router?.routeManifest?.find((entry) => entry.pageId === pageId); + notifyPageWillChange(pageId); + const legacyPageElement = document.getElementById(`${pageId}-page`); + if (route?.kind === 'react' || !legacyPageElement) { + showReactHost(pageId); + setActivePageChrome(pageId); + } else { + activatePage(pageId, { forceReload: options.forceReload === true }); + } if (!options.skipPushState) { const urlPath = pageId === 'dashboard' ? '/' : '/' + pageId; diff --git a/webui/static/library.js b/webui/static/library.js index 783c16ad..434b9319 100644 --- a/webui/static/library.js +++ b/webui/static/library.js @@ -700,6 +700,27 @@ let artistDetailPageState = { enhancedTrackSort: {} }; +function clearArtistDetailPageState() { + if (artistDetailPageState.completionController) { + artistDetailPageState.completionController.abort(); + artistDetailPageState.completionController = null; + } + + artistDetailPageState.currentArtistId = null; + artistDetailPageState.currentArtistName = null; + artistDetailPageState.currentArtistSource = null; + artistDetailPageState.originStack = []; +} + +if (typeof window !== 'undefined') { + window.addEventListener(PAGE_WILL_CHANGE_EVENT, (event) => { + const detail = event.detail || {}; + if (detail.fromPageId === 'artist-detail' && detail.toPageId !== 'artist-detail') { + clearArtistDetailPageState(); + } + }); +} + // Discography filter state let discographyFilterState = { categories: { albums: true, eps: true, singles: true }, diff --git a/webui/static/search.js b/webui/static/search.js index 9c6dff9e..8362e774 100644 --- a/webui/static/search.js +++ b/webui/static/search.js @@ -1161,6 +1161,9 @@ async function startDownload(index) { async function loadInitialData() { try { + const initialPath = window.location.pathname; + const initialNavigationEpoch = navigationEpoch; + // Load artist bubble state first await hydrateArtistBubblesFromSnapshot(); @@ -1177,6 +1180,10 @@ async function loadInitialData() { ? urlPage : homePage; + if (window.location.pathname !== initialPath || navigationEpoch !== initialNavigationEpoch) { + return; + } + // Always apply the target page to the legacy shell chrome. const router = getWebRouter(); const route = router?.routeManifest?.find((entry) => entry.pageId === targetPage); diff --git a/webui/static/shell-bridge.js b/webui/static/shell-bridge.js index 00e63e8a..e161cbf7 100644 --- a/webui/static/shell-bridge.js +++ b/webui/static/shell-bridge.js @@ -61,6 +61,7 @@ function activateLegacyPath(pathname) { return; } + notifyPageWillChange(targetPage); activatePage(targetPage, { forceReload: true }); } @@ -77,6 +78,7 @@ function syncActivePageFromLocation() { return; } + notifyPageWillChange(targetPage); const route = router?.routeManifest?.find((entry) => entry.pageId === targetPage); if (route?.kind === 'react') { showReactHost(targetPage); diff --git a/webui/tests/issues.smoke.spec.ts b/webui/tests/issues.smoke.spec.ts index c791c014..c816b8e6 100644 --- a/webui/tests/issues.smoke.spec.ts +++ b/webui/tests/issues.smoke.spec.ts @@ -13,10 +13,11 @@ async function selectProfile(page: Page, baseURL: string, profileId = 1) { async function waitForShellRoute(page: Page, pageId: string) { if (pageId === 'issues') { await expect - .poll(async () => - page.evaluate(() => document.querySelector('.page.active')?.id ?? ''), - ) + .poll(async () => page.evaluate(() => document.querySelector('.page.active')?.id ?? ''), { + timeout: 15000, + }) .toBe('webui-react-root'); + await expect(page.getByTestId('issues-board')).toBeVisible({ timeout: 15000 }); return; } @@ -106,3 +107,27 @@ test('browser history restores top-level routes', async ({ page, baseURL }) => { await waitForShellRoute(page, 'issues'); await expect(page).toHaveURL(/\/issues(?:\?status=open&category=all)?$/); }); + +test('browser history leaves artist detail when going back to library', async ({ + page, + baseURL, +}) => { + if (!baseURL) { + test.skip(); + return; + } + + await selectProfile(page, baseURL); + + await page.goto(new URL('/library', baseURL).toString(), { waitUntil: 'domcontentloaded' }); + await waitForShellRoute(page, 'library'); + await expect.poll(async () => page.locator('.library-artist-card').count()).toBeGreaterThan(0); + + await page.locator('.library-artist-card').first().click(); + await waitForShellRoute(page, 'artist-detail'); + await expect(page).toHaveURL(/\/artist-detail$/); + + await page.goBack(); + await waitForShellRoute(page, 'library'); + await expect(page).toHaveURL(/\/library$/); +});