fix(player): related marks survive, Restart restarts, and refused pages offer nothing to click

- Every state and Watch-later write goes through features/video/videoWrites,
  which marks the never-refetched lists (related lists, live searches) once
  the server has it; a contract test holds it. The Notification Center's
  undo, the feed's reset and the Watch-later toggles skipped it before.
- Confirmed marks are also kept apart (qk.videoMarks) and laid over the
  related rows: a Show more page landing later rebuilt them from the pages
  it read when it started, and dropped a mark made meanwhile.
- Restart on a related video starts at 0 (the session's stored position
  won before), and on the one already playing it seeks.
- The watched toggle remembers each video's mark: Back showed the opened
  video's launch-time state after a related one was marked.
- The help sheet leaves out the player's keys while a layer above it has
  them waiting.
- After a refusal a retry cannot answer, neither Show more (related) nor
  Load more (live search) is offered; a failed live-search page keeps the
  results already read and says why under them.
- The related cap message counts requests, as the cap does.
This commit is contained in:
2026-10-25 02:50:34 +02:00
parent ed51f5be7e
commit 4b5b66343d
26 changed files with 667 additions and 93 deletions
@@ -9,6 +9,7 @@ import KeyboardHelp from "./KeyboardHelp";
import { useContextMenu } from "./useContextMenu";
import {
closeKeyboardHelp,
setScopeCoveredProbe,
useGlobalKeymap,
useHelpConditions,
useKeyScope,
@@ -179,6 +180,33 @@ describe("KeyboardHelp", () => {
expect(descriptions()).not.toContain("keys.contextMenu");
});
// The YouTube player's keys wait while a layer sits above it (its related drawer, a dialog): a
// sheet opened there leaves them out, and one opened over the player itself lists them.
it("leaves out a surface's keys while a layer above it has them waiting", () => {
let covered = true;
setScopeCoveredProbe("ytPlayer", () => covered);
try {
render(
<Host>
<Surface scope="ytPlayer" />
</Host>,
);
const id = pushLayer();
pressH();
removeLayer(id);
expect(descriptions()).not.toContain("keys.playPause");
expect(descriptions()).toContain("keys.help");
act(() => closeKeyboardHelp());
covered = false;
pressH();
expect(descriptions()).toContain("keys.playPause");
} catch (e) {
setScopeCoveredProbe("ytPlayer", null);
throw e;
}
setScopeCoveredProbe("ytPlayer", null);
});
// Two live context menus (the Playlists page mounts one, its rail another) must not double the row.
it("lists the context-menu chords once however many surfaces register", () => {
function WithMenu() {
+6 -1
View File
@@ -10,6 +10,7 @@ import {
useHelpConditionValues,
useKeyboardHelpOpen,
useKeyboardHelpOverLayer,
useKeyboardHelpCovered,
useNavigablePages,
} from "../../lib/keymap";
@@ -33,6 +34,7 @@ export default function KeyboardHelp() {
const scopes = useActiveScopes();
const conditions = useHelpConditionValues();
const overLayer = useKeyboardHelpOverLayer();
const covered = useKeyboardHelpCovered();
const pages = useNavigablePages();
if (!open) return null;
@@ -47,7 +49,10 @@ export default function KeyboardHelp() {
const inLayer = (s: (typeof scopes)[number]) =>
(!isPageLevelScope(s) && s !== "contextMenu") ||
(overLayer === "layerWithList" && (s === "list" || s === "contextMenu"));
const live = overLayer === "page" ? scopes : scopes.filter(inLayer);
// A surface under a layer of its own (the player under its related drawer) waits: not listed.
const live = (overLayer === "page" ? scopes : scopes.filter(inLayer)).filter(
(s) => !covered.has(s),
);
const lines = helpLines(live, pages).filter((l) => !l.when || !!conditions[l.when]);
const words = { Space: t("keys.cap.space"), ContextMenu: t("keys.cap.menu") };
return (