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:
2026-10-26 01:13:39 +01:00
parent 91d36b3357
commit d4f24de4ba
34 changed files with 728 additions and 130 deletions
+36 -8
View File
@@ -1,4 +1,4 @@
import { useCallback, useState } from "react";
import { useCallback, useEffect, useState } from "react";
// Central registry of every key the app keeps in the browser (localStorage, and a tab's
// sessionStorage). One place to see them all, rename safely, and avoid collisions — instead of
@@ -307,10 +307,11 @@ export function useAccountPersistedState(
// --- a page's sub-tab, per browser tab ---------------------------------------------------------
// Which sub-tab a page was left on (Settings, Configuration, Statistics, Users) is kept for THIS browser
// tab only — sessionStorage, the account's (see ownKey): it survives a reload and a trip to another page, and a new
// Which sub-tab a page was left on (Settings, Configuration, Users) is kept for THIS browser tab only —
// sessionStorage, the account's (see ownKey): it survives a reload and a trip to another page, and a new
// tab opens the page on its first sub-tab (the owner's call: a position remembered forever in
// localStorage made Configuration open on "Plex" in every fresh tab, as if that were its home).
// localStorage made Configuration open on "Plex" in every fresh tab, as if that were its home). The
// address names the tab too (the page mirrors it): a link or a bookmark that carries one wins.
/** The sub-tab stored for this browser tab, or null. */
function getTabRaw(base: string): string | null {
@@ -322,8 +323,7 @@ function getTabRaw(base: string): string | null {
}
}
/** Pre-select a page's sub-tab before navigating there (a "Review" link, the Google-link return). */
export function setTabRaw(base: string, value: string): void {
function writeTabRaw(base: string, value: string): void {
const k = ownKey(base);
if (!k) return;
try {
@@ -333,10 +333,23 @@ export function setTabRaw(base: string, value: string): void {
}
}
/** `useAccountPersistedState` for a page's sub-tab, kept per browser tab (see above). */
// A pre-selection made while the page is already on screen (a notice's link opened over Settings) has
// no mount to be read at, and the navigation to the same page is a no-op: the page's hook hears it.
const TAB_SELECT_EVENT = "siftlode:tab-select";
type TabSelect = { base: string; value: string };
/** Pre-select a page's sub-tab before navigating there (a "Review" link, the Google-link return). */
export function setTabRaw(base: string, value: string): void {
writeTabRaw(base, value);
window.dispatchEvent(new CustomEvent<TabSelect>(TAB_SELECT_EVENT, { detail: { base, value } }));
}
/** `useAccountPersistedState` for a page's sub-tab, kept per browser tab (see above). `urlKey`: the
* address-bar key the page mirrors its tab to — a tab it names is the one the page opens on. */
export function useTabPersistedState(
base: string,
fallback = "",
urlKey?: string,
): [string, (value: string) => void] {
const [value, setValue] = useState<string>(() => {
// The position used to live in localStorage; a value left there would never be read again.
@@ -348,12 +361,27 @@ export function useTabPersistedState(
/* ignore */
}
}
const linked = urlKey ? new URLSearchParams(window.location.search).get(urlKey) : null;
if (linked) {
// Kept for this browser tab too, so a later trip back to the page (its address cleared by the
// page switch) still opens it here.
writeTabRaw(base, linked);
return linked;
}
return getTabRaw(base) ?? fallback;
});
useEffect(() => {
function onSelect(e: Event) {
const { detail } = e as CustomEvent<TabSelect>;
if (detail.base === base) setValue(detail.value);
}
window.addEventListener(TAB_SELECT_EVENT, onSelect);
return () => window.removeEventListener(TAB_SELECT_EVENT, onSelect);
}, [base]);
const set = useCallback(
(next: string) => {
setValue(next);
setTabRaw(base, next);
writeTabRaw(base, next);
},
[base],
);