fix(nav): a linked player, file and search survive what the app does by itself (review round 2)

- Playlists: a link over a playlist the account does not hold plays alone; the rail's own
  fallback no longer counts as a pick that gives the player up.
- The linked opener opens or clears nothing once the address names another player, and stops
  looking (and fetching its row) once it settled.
- Files: a list the server's cap cut — the sorted or searched one too — keeps the link, with a
  notice of its own (EN/HU).
- History: an in-place view rewrite (search refine, count, channel to channel, sub-view)
  drops a reloaded entry's overlay marks (replaceView); a landing on a channel opened from a
  search keeps the search; the Feed press on a search opens the plain feed.
- Files/Playlists: an emptied search is written at once.
- Client log: plf/cf (column text filters) are typed keys; the shell has none of its own.
- Contract: writers outside lib/ take a literal or a lib/ function's result; every URL key is
  sorted typed or not; the writer regexes are held by a fixture; sub-views named; the bookmark
  net reads page tags.
- Feed defaults in one constant; one live-search queue helper; Scheduler writes through the
  Activity codec; notRun copy (EN/HU) names Forward and a plain "Search YouTube" button.
This commit is contained in:
2026-10-27 16:08:52 +01:00
parent 0d5c6273f1
commit 6d57d62ac8
38 changed files with 717 additions and 187 deletions
@@ -14,7 +14,8 @@ import { notifyActionError } from "../../lib/actionErrors";
*
* `scope` names the list the address put it over (a playlist, a live search): when the host moves to
* another before it opened, the address under it named something else, and the player is given up —
* opened later, it would start over a list nobody linked, unasked. */
* opened later, it would start over a list nobody linked, unasked. So it is when a player opened
* meanwhile (a card clicked while the row was on its way): the address names that one now. */
export function useOpenLinkedPlayer(
ready: boolean,
find: (id: string) => Video | undefined,
@@ -24,14 +25,17 @@ export function useOpenLinkedPlayer(
const { t } = useTranslation();
const [linked] = useState(() => linkedPlayer()?.watch ?? null);
const [linkedScope] = useState(scope);
// Opened (or said to be gone) once; a ref, as nothing on screen reads it.
// Opened, said to be gone or given up, once. A ref for the effect (a StrictMode re-run of it must
// not open it twice); the state stops the lookup and the row fetch on screen after it.
const done = useRef(linked == null);
const [settled, setSettled] = useState(linked == null);
const moved = scope !== linkedScope;
const listed = ready && !moved && linked != null ? find(linked) : undefined;
const waiting = ready && !moved && !settled && linked != null;
const listed = waiting ? find(linked) : undefined;
const row = useQuery({
queryKey: qk.videoRow(linked ?? ""),
queryFn: () => api.videoRow(linked!),
enabled: ready && !moved && linked != null && !listed,
enabled: waiting && !listed,
retry: false,
});
const openIt = useEffectEvent(open);
@@ -41,12 +45,18 @@ export function useOpenLinkedPlayer(
});
useEffect(() => {
if (done.current) return;
if (moved) {
const settle = () => {
done.current = true;
// The address goes on naming it under the new list (a reload would open it there) — unless a
// player opened since names its own.
if (linkedPlayer()?.watch === linked) clearParams(...PLAYER_KEYS);
return;
// State, not derived: a one-way decision taken as the list or the row arrives. Derived from
// `listed`, the lookup and the row fetch would come back whenever the video left the list.
setSettled(true);
};
// A player opened since names its own: this one is not wanted any more.
if (linkedPlayer()?.watch !== linked) return settle();
if (moved) {
// The address goes on naming it under the new list (a reload would open it there).
clearParams(...PLAYER_KEYS);
return settle();
}
// Not before the host is ready: the row's cache entry is shared, so another opener's fetch fills
// it here too (a disabled query still reads the cache) — the shell's player over a page that
@@ -56,6 +66,6 @@ export function useOpenLinkedPlayer(
else if (row.data) openIt(row.data, false);
else if (row.isError) failed(row.error);
else return;
done.current = true;
settle();
}, [linked, moved, ready, listed, row.data, row.isError, row.error]);
}