Artist detail pages previously always pushed /artist-detail to the URL, so refreshing the page or sharing a link would drop users on a broken empty page with no artist loaded. URL format is now /artist-detail/:source/:id (e.g. /artist-detail/spotify/4tZwfgrHOc3mvqsCAfo4LT or /artist-detail/library/42). The source segment lets the backend synthesize a response from the right metadata client without a DB hit. Changes: Client routing (legacy shell + TanStack bridge) - buildArtistDetailPath / _getDeepLinkArtistDetail added to init.js; parse both new :source/:id and legacy bare :id formats so old bookmarks still work - navigateToPage passes artistId + artistSource through to the router bridge, which builds the dynamic href instead of hardcoding route.path - resolveShellPageFromPath / resolveLegacyShellPageFromPath use a prefix match so /artist-detail/* resolves to artist-detail page-id - globals.d.ts typed for artistId / artistSource options - activateLegacyPath and syncActivePageFromLocation (popstate) both restore artist from URL using skipRouteChange:true to avoid a re-navigation loop back to /artist-detail - loadInitialData restores artist from URL on page load (router not yet mounted at DOMContentLoaded so legacy path runs unconditionally) - Same-artist guard in navigateToArtistDetail prevents double-fetch when the router fires activateLegacyPath after the initial navigation Server - artist_source_detail.build_source_only_artist_detail now resolves artist name from the source API when none is supplied, so deep-link restores with an empty name string still render correctly Tests - test_spa_deep_linking: /artist-detail/42 and /artist-detail/spotify/ID both serve index.html - bridge.test.ts: source-aware URL building and library fallback - route-manifest.test.ts: prefix path resolution - artist_source_detail: name resolved from source when input is empty
194 lines
6.5 KiB
JavaScript
194 lines
6.5 KiB
JavaScript
// SoulSync shell bridge glue
|
|
// Keep this file loaded after init.js so the legacy shell runtime state is ready.
|
|
|
|
function getWebRouter() {
|
|
return window.SoulSyncWebRouter ?? null;
|
|
}
|
|
|
|
function showLegacyPage(pageId) {
|
|
document.querySelectorAll('.page').forEach(page => {
|
|
page.classList.remove('active');
|
|
});
|
|
const page = document.getElementById(`${pageId}-page`);
|
|
if (page) {
|
|
page.classList.add('active');
|
|
}
|
|
const reactHost = document.getElementById('webui-react-root');
|
|
if (reactHost) {
|
|
reactHost.classList.remove('active');
|
|
}
|
|
}
|
|
|
|
function setActivePageChrome(pageId) {
|
|
document.querySelectorAll('.nav-button').forEach(btn => {
|
|
btn.classList.remove('active');
|
|
});
|
|
const navButton = document.querySelector(`[data-page="${pageId}"]`);
|
|
if (navButton) {
|
|
navButton.classList.add('active');
|
|
} else if (pageId === 'artist-detail') {
|
|
// Artist detail is a Library context, so keep the sidebar anchored there.
|
|
const libraryBtn = document.querySelector('[data-page="library"]');
|
|
if (libraryBtn) {
|
|
libraryBtn.classList.add('active');
|
|
}
|
|
}
|
|
currentPage = pageId;
|
|
if (typeof _updateSidebarLibraryBreadcrumb === 'function') _updateSidebarLibraryBreadcrumb();
|
|
if (typeof _gsUpdateVisibility === 'function') _gsUpdateVisibility();
|
|
const downloadSidebar = document.getElementById('discover-download-sidebar');
|
|
if (downloadSidebar) {
|
|
if (pageId === 'discover') {
|
|
const activeDownloads = typeof discoverDownloads !== 'undefined'
|
|
? Object.keys(discoverDownloads).length
|
|
: 0;
|
|
if (activeDownloads > 0 && typeof updateDiscoverDownloadBar === 'function') {
|
|
updateDiscoverDownloadBar();
|
|
}
|
|
} else {
|
|
downloadSidebar.classList.add('hidden');
|
|
}
|
|
}
|
|
if (window.pageParticles && window._particlesEnabled !== false) window.pageParticles.setPage(pageId);
|
|
if (window.workerOrbs) window.workerOrbs.setPage(pageId);
|
|
}
|
|
|
|
function showReactHost(pageId) {
|
|
document.querySelectorAll('.page').forEach(page => {
|
|
page.classList.remove('active');
|
|
});
|
|
const host = document.getElementById('webui-react-root');
|
|
if (host) {
|
|
host.classList.add('active');
|
|
}
|
|
currentPage = pageId;
|
|
if (typeof _gsUpdateVisibility === 'function') _gsUpdateVisibility();
|
|
if (window.pageParticles && window._particlesEnabled !== false) window.pageParticles.setPage(pageId);
|
|
if (window.workerOrbs) window.workerOrbs.setPage(pageId);
|
|
}
|
|
|
|
function activateLegacyPath(pathname) {
|
|
const router = getWebRouter();
|
|
const targetPage = router?.resolvePageId?.(pathname) || _getPageFromPath(pathname);
|
|
if (!targetPage) return;
|
|
|
|
if (!isPageAllowed(targetPage)) {
|
|
const home = getProfileHomePage();
|
|
if (home !== targetPage) {
|
|
navigateToPage(home, { replace: true });
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (targetPage === 'artist-detail') {
|
|
const deepArtist = _getDeepLinkArtistDetail(pathname);
|
|
if (deepArtist?.artistId && typeof navigateToArtistDetail === 'function') {
|
|
navigateToArtistDetail(deepArtist.artistId, '', deepArtist.source === 'library' ? null : deepArtist.source, { skipOriginPush: true, skipRouteChange: true });
|
|
return;
|
|
}
|
|
navigateToPage('library', { replace: true });
|
|
return;
|
|
}
|
|
|
|
notifyPageWillChange(targetPage);
|
|
activatePage(targetPage, { forceReload: true });
|
|
}
|
|
|
|
function syncActivePageFromLocation() {
|
|
const router = getWebRouter();
|
|
const targetPage = router?.resolvePageId?.(window.location.pathname) || _getPageFromPath(window.location.pathname);
|
|
if (!targetPage) return;
|
|
|
|
if (!isPageAllowed(targetPage)) {
|
|
const home = getProfileHomePage();
|
|
if (home !== targetPage) {
|
|
navigateToPage(home, { replace: true });
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (targetPage === 'artist-detail') {
|
|
const deepArtist = _getDeepLinkArtistDetail();
|
|
if (deepArtist?.artistId && typeof navigateToArtistDetail === 'function') {
|
|
navigateToArtistDetail(deepArtist.artistId, '', deepArtist.source === 'library' ? null : deepArtist.source, { skipOriginPush: true, skipRouteChange: true });
|
|
return;
|
|
}
|
|
navigateToPage('library', { replace: true });
|
|
return;
|
|
}
|
|
|
|
notifyPageWillChange(targetPage);
|
|
const route = router?.routeManifest?.find((entry) => entry.pageId === targetPage);
|
|
if (route?.kind === 'react') {
|
|
showReactHost(targetPage);
|
|
} else {
|
|
showLegacyPage(targetPage);
|
|
}
|
|
setActivePageChrome(targetPage);
|
|
}
|
|
|
|
const SHELL_BRIDGE_READY_EVENT = 'ss:webui-shell-bridge-ready';
|
|
|
|
function openDownloadMissingAlbumWorkflow(input) {
|
|
if (typeof openDownloadMissingModalForArtistAlbum !== 'function') {
|
|
throw new Error('Download workflow host is not ready yet');
|
|
}
|
|
|
|
return openDownloadMissingModalForArtistAlbum(
|
|
input.virtualPlaylistId,
|
|
input.playlistName,
|
|
input.tracks,
|
|
input.album,
|
|
input.artist,
|
|
false,
|
|
);
|
|
}
|
|
|
|
function openAddToWishlistAlbumWorkflow(input) {
|
|
if (typeof openAddToWishlistModal !== 'function') {
|
|
throw new Error('Wishlist workflow host is not ready yet');
|
|
}
|
|
|
|
return openAddToWishlistModal(input.album, input.artist, input.tracks, input.albumType);
|
|
}
|
|
|
|
window.SoulSyncWorkflowActions = {
|
|
openDownloadMissingAlbum: openDownloadMissingAlbumWorkflow,
|
|
openAddToWishlistAlbum: openAddToWishlistAlbumWorkflow,
|
|
notify(message, type) {
|
|
if (typeof showToast === 'function') {
|
|
showToast(message, type);
|
|
}
|
|
},
|
|
};
|
|
|
|
window.SoulSyncWebShellBridge = {
|
|
getCurrentProfileContext() {
|
|
if (!currentProfile) return null;
|
|
return {
|
|
profileId: currentProfile.id,
|
|
isAdmin: !!currentProfile.is_admin,
|
|
};
|
|
},
|
|
isPageAllowed(pageId) {
|
|
return isPageAllowed(pageId);
|
|
},
|
|
getProfileHomePage() {
|
|
return getProfileHomePage();
|
|
},
|
|
resolveLegacyPath(pathname) {
|
|
return getWebRouter()?.resolvePageId?.(pathname) ?? null;
|
|
},
|
|
setActivePageChrome(pageId) {
|
|
setActivePageChrome(pageId);
|
|
},
|
|
activateLegacyPath(pathname) {
|
|
activateLegacyPath(pathname);
|
|
},
|
|
showReactHost(pageId) {
|
|
showReactHost(pageId);
|
|
},
|
|
};
|
|
|
|
window.addEventListener('popstate', syncActivePageFromLocation);
|
|
window.dispatchEvent(new CustomEvent(SHELL_BRIDGE_READY_EVENT));
|