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
+19 -9
View File
@@ -41,6 +41,13 @@ import { playlistName } from "../../lib/playlistName";
import { reinsertAt } from "../../lib/reinsert";
import { useUndoable } from "../../lib/useUndoable";
import { cycleSort, type SortState } from "../../lib/columnSort";
import { useUrlMirror } from "../../lib/useUrlMirror";
import {
isStoredSort,
PLAYLIST_STORED_SORT,
playlistViewFrom,
playlistViewParamUpdates,
} from "../../lib/playlistsUrlState";
import { formatRuntime } from "../../lib/format";
import { applyView, sameOrder } from "../../lib/playlistView";
const PlayerModal = lazy(() => import("../../features/player/PlayerModal"));
@@ -95,11 +102,8 @@ import PlaylistsHeader from "./PlaylistsHeader";
// It composes `tableHeader`'s pieces instead of rendering a `DataTable`, because DataTable owns
// its own filter/sort/page state and the stored order needs a single owner.
// The neutral view: "#" ascending IS the stored order, so the header can show a real arrow for it
// instead of the list looking unsorted (or, worse, looking sorted by whatever column the stored
// order happens to correlate with — the user read a channel-grouped playlist as sorted by Channel).
const STORED_SORT: SortState = { key: "pos", dir: "asc" };
const isStoredSort = (s: SortState) => s?.key === "pos" && s.dir === "asc";
// The neutral view ("#" ascending, the stored order) lives beside the address-bar codec of the view.
const STORED_SORT = PLAYLIST_STORED_SORT;
const idsKey = (items: Video[]) =>
items
@@ -232,10 +236,13 @@ function PlaylistDetail() {
},
});
const items = order.value;
// VIEW state — never written to the server on its own.
const [viewSort, setViewSort] = useState<SortState>(STORED_SORT);
const [filters, setFilters] = useState<FilterMap>({});
const [groupBy, setGroupBy] = useState(false);
// VIEW state — never written to the server on its own. The address names it, so a reload or a
// bookmark opens the playlist sorted, filtered and grouped as it was left (the view it opens on is
// the address's; a switch to another playlist starts over, below).
const [opened] = useState(() => playlistViewFrom(new URLSearchParams(window.location.search)));
const [viewSort, setViewSort] = useState<SortState>(opened.sort);
const [filters, setFilters] = useState<FilterMap>(opened.filters);
const [groupBy, setGroupBy] = useState(opened.groupBy);
// Tracks the membership (id set) currently loaded into `order`, so a refetch that only
// re-confirms our own reorder doesn't wipe the undo history — we reset history only when
// the actual set of items changes (different playlist, or an add/remove).
@@ -354,6 +361,9 @@ function PlaylistDetail() {
setFilters({});
setGroupBy(false);
}
useUrlMirror(
playlistViewParamUpdates(selectedId != null ? { sort: viewSort, filters, groupBy } : null),
);
const builtin = detail?.kind === "watch_later";
// YouTube-mirrored playlists can be edited locally and synced back (the read-sync skips a