feat(nav): full bookmarkable URL model — retire minimalist-URL + Share-view (E4 S2)
The query string is now the source of truth for the current view, so every URL is bookmarkable/shareable. Reverses the early minimalist-URL rule (user's call). - urlState: urlWithParams (read-modify-write delta on the live query, each provider owns its keys) + clearParams (strip only transient auth params) replace the whole- query stripUrlParams; shareUrl deleted; filterParamUpdates helper. - NavigationProvider: page + ?chan mirrored to the address bar (pushState URL arg; history.state mechanics untouched); loadInitialPage resolves deep links (?v->plex, ?pl->playlists, filter/?chan->feed, bare->stored landing). - FeedFiltersProvider: feed filters mirrored to the URL, gated on page===feed. - useHistorySubview: optional SubviewUrl mapping; PlexBrowse mirrors the sub-view to ?v=<kind>:<args> (show/season/info/person/playlist; grid+player stay ephemeral). - PlaylistsProvider: selected playlist mirrored to ?pl on the Playlists page. - App: the 5 stripUrlParams calls become targeted clearParams (link/oauth/admin/ access/verify/reset); the share-link hydrate effect removed. - Retire the "Share view" feature: Sidebar + SavedViewsWidget buttons + i18n keys. Browser tab title stays the short module/channel name (document.title untouched) — the query never leaks into it, per the user's request. Gate green: typecheck, knip, eslint (0 err), prettier, 247 vitest (urlState.dom.test +9). No browser-E2E yet (most visual sprint — flagged for the epic-end review).
This commit is contained in:
@@ -1,6 +1,16 @@
|
||||
import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from "react";
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { getAccountRaw, LS, setAccountRaw } from "../lib/storage";
|
||||
import { useAccountRehydrate } from "../lib/useAccountRehydrate";
|
||||
import { urlWithParams } from "../lib/urlState";
|
||||
import { useNavigation } from "./NavigationProvider";
|
||||
|
||||
// Owns the Playlists module's shared local state — the rail's name filter and the selected playlist
|
||||
// — lifted out of the App god component so the header search box, the PlaylistsRail, and the
|
||||
@@ -37,6 +47,9 @@ export function usePlaylistsActions(): PlaylistsActions {
|
||||
}
|
||||
|
||||
function readSelectedId(): number | null {
|
||||
// A ?pl bookmark wins over the stored selection so a pasted link opens that playlist.
|
||||
const fromUrl = new URLSearchParams(window.location.search).get("pl");
|
||||
if (fromUrl != null && Number.isFinite(Number(fromUrl))) return Number(fromUrl);
|
||||
const s = getAccountRaw(LS.playlist);
|
||||
return s ? Number(s) : null;
|
||||
}
|
||||
@@ -59,6 +72,18 @@ export function PlaylistsProvider({ children }: { children: ReactNode }) {
|
||||
[markDirty],
|
||||
);
|
||||
|
||||
// Mirror the selected playlist to ?pl on the Playlists page so it's bookmarkable; clear it elsewhere
|
||||
// (the localStorage copy still restores it on return). replaceState — selecting isn't a new history
|
||||
// entry — and urlWithParams keeps ?page and the other nav params intact.
|
||||
const { page } = useNavigation();
|
||||
useEffect(() => {
|
||||
window.history.replaceState(
|
||||
window.history.state,
|
||||
"",
|
||||
urlWithParams({ pl: page === "playlists" && selectedId != null ? String(selectedId) : null }),
|
||||
);
|
||||
}, [page, selectedId]);
|
||||
|
||||
const state = useMemo<PlaylistsState>(() => ({ search, selectedId }), [search, selectedId]);
|
||||
const actions = useMemo<PlaylistsActions>(
|
||||
() => ({ setSearch, setSelectedId }),
|
||||
|
||||
Reference in New Issue
Block a user