feat: URL-based deep linking for SPA navigation

- Flask catch-all route serves index.html for client-side paths, excluding api/static/auth/callback/status prefixes.- navigateToPage pushes history state so URL reflects current page.- popstate listener handles browser back/forward without reloading.- Initial load reads window.location to restore the page after refresh or direct link.- artist-detail and playlist-explorer fall back to parent pages since they need runtime context.
pull/328/head
JohnBaumb 4 weeks ago
parent dbaeba33dd
commit 7d311451cb

@ -21,7 +21,7 @@ from pathlib import Path
from urllib.parse import urljoin
from concurrent.futures import ThreadPoolExecutor, as_completed
from flask import Flask, render_template, request, jsonify, redirect, send_file, Response, session, g
from flask import Flask, render_template, request, jsonify, redirect, send_file, Response, session, g, abort
from flask_socketio import SocketIO, emit, join_room, leave_room
from utils.logging_config import get_logger, setup_logging
from utils.async_helpers import run_async
@ -4875,6 +4875,13 @@ def run_detection(server_type):
def index():
return render_template('index.html')
@app.route('/<path:page>')
def spa_catch_all(page):
# Serve index.html for client-side routes; let Flask handle real routes first.
if page.startswith(('api/', 'static/', 'auth/', 'callback', 'tidal/', 'status')):
abort(404)
return render_template('index.html')
# --- API Endpoints ---
# Tracks cumulative item-processed totals over time for windowed counting.

@ -2773,6 +2773,31 @@ function initializeNavigation() {
navigateToPage(page);
});
});
window.addEventListener('popstate', (event) => {
const page = (event.state && event.state.page) || _getPageFromPath();
if (page && page !== currentPage) {
navigateToPage(page, { skipPushState: true });
}
});
}
const _DEEPLINK_VALID_PAGES = new Set([
'dashboard', 'sync', 'downloads', 'discover', 'artists', 'automations',
'library', 'import', 'settings', 'help', 'issues', 'stats', 'watchlist',
'wishlist', 'active-downloads', 'artist-detail', 'playlist-explorer',
'hydrabase', 'tools'
]);
function _getPageFromPath() {
const path = window.location.pathname.replace(/^\/+|\/+$/g, '');
if (!path) return 'dashboard';
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 === 'playlist-explorer') return 'library';
return basePage;
}
// ===============================
@ -2905,7 +2930,7 @@ function initializeDownloadManagerToggle() {
console.log('Download manager toggle initialized');
}
function navigateToPage(pageId) {
function navigateToPage(pageId, options = {}) {
if (pageId === currentPage) return;
// Permission guard — redirect to home page if not allowed
@ -2937,6 +2962,13 @@ function navigateToPage(pageId) {
currentPage = pageId;
if (!options.skipPushState) {
const urlPath = pageId === 'dashboard' ? '/' : '/' + pageId;
if (window.location.pathname !== urlPath) {
history.pushState({ page: pageId }, '', urlPath);
}
}
// Show/hide global search bar (hide on downloads page where enhanced search exists)
if (typeof _gsUpdateVisibility === 'function') _gsUpdateVisibility();
@ -10233,8 +10265,15 @@ async function loadInitialData() {
// Navigate to user's home page (or dashboard for admin)
const homePage = getProfileHomePage();
if (homePage !== 'dashboard') {
navigateToPage(homePage);
const urlPage = _getPageFromPath();
const targetPage = (urlPage && urlPage !== 'dashboard' && isPageAllowed(urlPage))
? urlPage
: homePage;
history.replaceState({ page: targetPage }, '', (targetPage === 'dashboard' ? '/' : '/' + targetPage) + window.location.search + window.location.hash);
if (targetPage !== 'dashboard') {
navigateToPage(targetPage, { skipPushState: true });
} else {
await loadDashboardData();
loadDashboardSyncHistory();

Loading…
Cancel
Save