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
@@ -49,20 +49,30 @@ function Probe() {
channels: () => channels.setView("hidden"),
files: () => files.setView({ kind: "folder", id: 3 }),
playlists: () => playlists.setSelectedId(5),
"plex search": () => plex.setQ("poirot"),
"channels search": () => channels.setSearch("tom"),
"files search": () => files.setSearch("notes"),
"playlists search": () => playlists.setSearch("jazz"),
},
};
});
return null;
}
// What each page's change above writes, once the page is what the address shows.
const WRITTEN: Record<string, [string, string]> = {
feed: ["sort", "oldest"],
plex: ["pscope", "movie"],
channels: ["ctab", "hidden"],
files: ["folder", "3"],
playlists: ["pl", "5"],
// What each change above writes, once its page is what the address shows: [page, key, value]. A
// search is written once the typing pauses.
const WRITTEN: Record<string, [string, string, string]> = {
feed: ["feed", "sort", "oldest"],
plex: ["plex", "pscope", "movie"],
channels: ["channels", "ctab", "hidden"],
files: ["files", "folder", "3"],
playlists: ["playlists", "pl", "5"],
"plex search": ["plex", "pq", "poirot"],
"channels search": ["channels", "cq", "tom"],
"files search": ["files", "fq", "notes"],
"playlists search": ["playlists", "plq", "jazz"],
};
const PAUSE = () => new Promise((r) => setTimeout(r, 400));
beforeEach(() => {
localStorage.clear();
@@ -75,8 +85,9 @@ afterEach(() => {
const search = () => new URLSearchParams(window.location.search);
describe.each(Object.keys(WRITTEN))("a channel page over %s", (page) => {
describe.each(Object.keys(WRITTEN))("a channel page over %s", (change) => {
it("keeps the page's keys off its address, and they come back when it closes", async () => {
const [page, key, value] = WRITTEN[change]!;
// The page named even for the feed: a bare address opens on Home (the account's default).
window.history.replaceState({ sfPage: page }, "", `/?page=${page}`);
render(
@@ -89,8 +100,8 @@ describe.each(Object.keys(WRITTEN))("a channel page over %s", (page) => {
</QueryClientProvider>,
);
act(() => probe.current!.nav.openChannel("UC1", "One"));
act(() => probe.current!.change[page]!());
const [key, value] = WRITTEN[page]!;
act(() => probe.current!.change[change]!());
await act(PAUSE);
expect(search().get(key)).toBeNull();
expect(search().get("chan")).toBe("UC1");
act(() => probe.current!.nav.closeChannel());