feat(nav): tabs, list views and name searches are named in the address
Settings, Configuration and Users name their tab (stab/cfgtab/utab); a link's tab wins and a tab pre-selected while its page is on screen now switches it. A playlist's sort, filters and grouping (plsort/plf/plgroup), the Files sort (fsort, always written) and the name searches of Channels, Files, Playlists and Plex (cq/fq/plq/pq+pqf) reopen from the address; the searches are written once the typing pauses.
This commit is contained in:
@@ -7,11 +7,14 @@ import {
|
||||
hasPlexParams,
|
||||
plexParamsToState,
|
||||
plexParamUpdates,
|
||||
plexSearchFrom,
|
||||
plexSearchParamUpdates,
|
||||
type PlexUrlState,
|
||||
} from "../../lib/plexUrlState";
|
||||
import { getAccountRaw, LS, setAccountRaw } from "../../lib/storage";
|
||||
import { logFilterChange } from "../../lib/filterLog";
|
||||
import { useUrlMirror } from "../../lib/useUrlMirror";
|
||||
import { useDebounced } from "../../lib/useDebounced";
|
||||
import { useNavigation } from "../../features/navigation/navigationContext";
|
||||
import { urlPage } from "../../features/navigation/urlPage";
|
||||
import {
|
||||
@@ -28,7 +31,7 @@ import {
|
||||
// instead of prop-drilling through App. Mirrors FeedFiltersProvider (split State + Actions contexts,
|
||||
// stable useCallback setters, one-line hooks; composed in main.tsx around <App/>).
|
||||
//
|
||||
// scope / show / sort / filters / q are per-account persisted (localStorage) and seeded from it at mount
|
||||
// scope / show / sort / filters / q / field are per-account persisted (localStorage) and seeded from it at mount
|
||||
// (BootGate claims the local state for the account first; a different account reloads the tab). `playlistOpen` is an ephemeral
|
||||
// hand-off (the rail sets it, PlexBrowse consumes it into a history subview then clears it). The Plex
|
||||
// search term is deliberately its OWN state, NOT the feed's `filters.q`, so a Plex search never leaks
|
||||
@@ -132,9 +135,13 @@ export function PlexProvider({ children }: { children: ReactNode }) {
|
||||
return EMPTY_PLEX_FILTERS;
|
||||
}
|
||||
}, [filtersRaw]);
|
||||
// Survives F5 (persisted), unlike the feed search — see the scope note above.
|
||||
const [q, setQState] = useState<string>(readPlexQ);
|
||||
const [field, setFieldState] = useState<PlexSearchField>(readPlexField);
|
||||
// Remembered per account (a trip back through the rail finds it), and named in the address: a link's
|
||||
// search wins and is remembered too.
|
||||
const [urlSearch] = useState(() => plexSearchFrom(new URLSearchParams(window.location.search)));
|
||||
const [q, setQState] = useState<string>(() => urlSearch.q ?? readPlexQ());
|
||||
const [field, setFieldState] = useState<PlexSearchField>(
|
||||
() => urlSearch.field ?? readPlexField(),
|
||||
);
|
||||
const [playlistOpen, setPlaylistOpen] = useState<number | null>(null);
|
||||
// Ephemeral hand-off like `playlistOpen`: the header typeahead sets the title to jump to;
|
||||
// PlexBrowse consumes it into an info/show history subview then clears it.
|
||||
@@ -153,6 +160,10 @@ export function PlexProvider({ children }: { children: ReactNode }) {
|
||||
if (present.sort) setAccountRaw(LS.plexSort, JSON.stringify(urlSeed.sort));
|
||||
if (present.filters) setAccountRaw(LS.plexFilters, JSON.stringify(urlSeed.filters));
|
||||
}, [urlSeed]);
|
||||
useEffect(() => {
|
||||
if (urlSearch.q != null) setAccountRaw(LS.plexQ, urlSearch.q);
|
||||
if (urlSearch.field != null) setAccountRaw(LS.plexField, urlSearch.field);
|
||||
}, [urlSearch]);
|
||||
|
||||
const setScope = useCallback((v: string) => {
|
||||
logFilterChange("plex", { scope: v });
|
||||
@@ -197,10 +208,14 @@ export function PlexProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
// Mirror the content state (scope + sort + watch-state + filters) into the address bar under the
|
||||
// namespaced keys so a Plex-browse view is shareable. Gated on urlPage===plex: off the Plex page (or
|
||||
// under a channel page) the keys are cleared (localStorage still holds the state; it re-mirrors on return). `q`/`field` stay OUT — the
|
||||
// search term is deliberately transient-per-view and not part of the shared four-key contract.
|
||||
// under a channel page) the keys are cleared (localStorage still holds the state; it re-mirrors on
|
||||
// return). The search beside them, once the typing pauses: one address-bar write per keystroke is
|
||||
// churn a browser throttles.
|
||||
const nav = useNavigation();
|
||||
useUrlMirror(plexParamUpdates(urlPage(nav) === "plex" ? { scope, sort, show, filters } : null));
|
||||
const shown = urlPage(nav) === "plex";
|
||||
useUrlMirror(plexParamUpdates(shown ? { scope, sort, show, filters } : null));
|
||||
const typed = useDebounced(q, 300);
|
||||
useUrlMirror(plexSearchParamUpdates(shown ? { q: typed, field } : null));
|
||||
|
||||
return (
|
||||
<PlexActionsContext.Provider value={actions}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Clapperboard } from "lucide-react";
|
||||
import { lazyWithPreload, lazyWithPreloadAlongside } from "../../lib/lazyWithPreload";
|
||||
import type { ModuleDef } from "../../lib/moduleDef";
|
||||
import { PLEX_KEYS, PLEX_SUB_KEY } from "../../lib/plexUrlState";
|
||||
import { PLEX_KEYS, PLEX_SEARCH_KEYS, PLEX_SUB_KEY } from "../../lib/plexUrlState";
|
||||
import { PlexStore } from "./PlexStore";
|
||||
|
||||
// The rail loads with the page (lazyWithPreloadAlongside), so it never arrives after it.
|
||||
@@ -18,5 +18,5 @@ export const plexModule: ModuleDef = {
|
||||
Page: lazyWithPreloadAlongside(() => import("./PlexBrowse"), Rail),
|
||||
maxWidth: "100rem",
|
||||
Store: PlexStore,
|
||||
urlKeys: [...PLEX_KEYS, PLEX_SUB_KEY],
|
||||
urlKeys: [...PLEX_KEYS, ...PLEX_SEARCH_KEYS, PLEX_SUB_KEY],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user