feat(e24): reach the Home surface — the landing toggle, a palette command, Shift+W

S4's reachability half. Home already led the nav order; what was missing was every
way IN other than the rail.

- Settings → Appearance → Startup: "Start on Home", the documented escape hatch for
  the landing override. Deliberately still localStorage and not a server pref —
  `loadInitialPage` answers before /api/me lands, and a landing module decided after
  the first paint is a visible jump. Its two stored values are now ONE exported
  constant, so the writer and the boot-time reader cannot drift on a magic string.
- The command palette gets "Meglepetés" / "Surprise me": it navigates to Home and
  rerolls the hero on arrival. No second "Mit nézzek?" row beside it — the Home
  module row already matches that phrase through its descriptive title.
- A global Shift+W jumps to Home. A chord, not a bare `w`: the Plex player binds bare
  letters and `w` sits on `s`, so a blind reach for the subtitle key would have thrown
  a viewer out of the film.

The palette's request crosses a lazy module boundary, so it is a token that waits
(lib/homeIntent) rather than an event fired into an empty room. Browser-measured:
taken with a compare-and-clear, because StrictMode's double-invoked mount effect
otherwise rerolled twice and landed on the page two picks deep.

HU/EN parity kept; UAT on localdev in both languages, including the empty shelf.
This commit is contained in:
2026-09-08 00:06:34 +02:00
parent 82914b56ca
commit 828dad015c
18 changed files with 238 additions and 29 deletions
+12 -1
View File
@@ -1,4 +1,4 @@
import { lazy, Suspense, useCallback, useState } from "react";
import { lazy, Suspense, useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { keepPreviousData, useQuery, useQueryClient } from "@tanstack/react-query";
import {
@@ -23,6 +23,7 @@ import {
type VideoStatus,
} from "../lib/api";
import { notifyActionError } from "../lib/actionErrors";
import { takeSurprise, useSurpriseRequest } from "../lib/homeIntent";
import { formatDuration } from "../lib/format";
import { toggleWatchState } from "../lib/plexWatch";
import { useComfortPin, pinTargetForCard, shelfMoveTarget, type ShelfMove } from "./comfortPin";
@@ -511,6 +512,16 @@ export default function Home() {
});
}, [heroKey]);
// "Meglepetés" from the command palette: the request is made on another page, so it waits as a
// token until this module's chunk has actually mounted (see lib/homeIntent). The subscribed value
// is what WAKES this effect; `takeSurprise()` is what decides — it clears the token and answers
// false to every later pass, including StrictMode's second mount invocation and the re-runs
// `reroll`'s changing identity causes.
const surpriseRequest = useSurpriseRequest();
useEffect(() => {
if (surpriseRequest && takeSurprise()) reroll();
}, [surpriseRequest, reroll]);
if (q.isPending) return <LoadingState />;
if (q.isError)
return (