fix(nav): the address follows the view through overlays, reloads and Back (review round 1)

- A URL mirror names the view again on every entry Back or Forward lands on, so a change made
  under a dialog, a drawer or a sub-view is not undone by the next reload.
- A view pushed from the entry a reload under a dialog started from no longer inherits its overlay
  mark; a reload under two overlays closes onto the page in one step.
- The linked player reopens over a live search, gives itself up when its list moves first, and
  plays alone over an emptied playlist; a capped Files list keeps the link it cannot resolve.
- The Feed press over a channel ends the search; leaving a search ends its asked mark; Files and
  Playlists searches end with their page; the feed always names its view and debounces its search.
- Search-box keys are declared per module and kept out of the client log below TRACE.
- Writers outside lib/ go through codecs; the URL contract scans computed keys, overlay writers and
  two-line reads, and holds every page to a bookmark round trip (Config, Activity, Playlists and the
  Files sort and viewer added).
This commit is contained in:
2026-10-27 13:52:42 +01:00
parent 7b4e007680
commit 0d5c6273f1
50 changed files with 928 additions and 157 deletions
+12 -11
View File
@@ -503,6 +503,10 @@ export default function Feed({
const saved = savedOverrides[v.id];
return saved && !(live && saved.answered) ? { ...v, saved: saved.value } : v;
});
// The live search's cards, in the order picked for them, and the player's queue over them.
const ytShown = (rows: Video[]): Video[] =>
sortYtResults(withOverrides(rows, true), ytSort.key as SortKey, ytSort.dir, lang);
const ytQueue = (rows: Video[]): Video[] => ytShown(rows).filter((v) => v.status !== "hidden");
const merged: Video[] = withOverrides(loaded);
const items: Video[] = merged.filter((v) => matchesView(v.status, filters.show));
// The in-app player's queue (auto-advance / loop / prev-next). It's the filtered feed, but the
@@ -511,16 +515,18 @@ export default function Feed({
// goes hidden still drops out ("visible affects").
const playerQueue: Video[] = merged.filter((v) => v.status !== "hidden");
// A reload or a bookmark of the player reopens it once this list is in, the list as its queue again
// (the feed's filters are in the address under it). Not over a live search: that is not in the
// address, and a reload leaves it.
// (the feed's filters are in the address under it) — over a live search too, its results the
// queue, once they are in (or the search waits for its press, and it plays alone). Scoped to the
// list it was linked over: leaving the search first gives it up.
useOpenLinkedPlayer(
!ytActive && !query.isLoading,
(id) => playerQueue.find((v) => v.id === id),
ytActive ? ytQuery.data !== undefined || ytQuery.isError || !ytRuns : !query.isLoading,
(id) => (ytActive ? ytQueue(ytLoaded) : playerQueue).find((v) => v.id === id),
(video, inList) => {
openVideo(video);
// Not in what this list has loaded (further down it, or no longer in it): it plays alone.
if (!inList) setActiveVideo((a) => a && { ...a, alone: true });
},
ytActive ? `yt:${ytSearch}` : "feed",
);
// A live search ingests new catalog videos, so the disabled feed queries still hold their stale
@@ -534,12 +540,7 @@ export default function Feed({
// A separate data source rendered in the same cards/player. No watch-state view filter (the
// user explicitly searched, so show everything) and no auto-pagination (each page costs quota).
if (ytActive) {
const ytItems: Video[] = sortYtResults(
withOverrides(ytLoaded, true),
ytSort.key as SortKey,
ytSort.dir,
lang,
);
const ytItems: Video[] = ytShown(ytLoaded);
const ytPlayerQueue: Video[] = ytItems.filter((v) => v.status !== "hidden");
const ytError =
ytQuery.error instanceof HttpError
@@ -704,7 +705,7 @@ export default function Feed({
<PlayerModal
video={activeVideo.video}
startAt={activeVideo.startAt}
queue={ytPlayerQueue}
queue={activeVideo.alone ? undefined : ytPlayerQueue}
onClose={closePlayer}
onState={onState}
onWrite={trackSessionWrite}