feat(design): players on touch and the C5 control set, restart on the card (E28 S3f2)

Plex player: a finger's tap on the picture only shows or hides the controls,
the hide timer starts with playback, a double-tap on a side third skips 10 s,
the seek bar is a 2.75rem touch-none hit area with its head shown on touch.
The row is play/pause, Back by position, Next only with a next item, mute,
time, tracks, a fixed ⋯ (settings, media info, stats, shortcuts, download)
and fullscreen; Stop, Restart and the ±10 s buttons are gone.

A started card shows the video card's Continue + Restart pair (one shared
MediaResumeButtons, beside the picture's button), and its menu lists the
restart; the player opens that item at 0 (fromStart). YouTube modal: the
description opens from an ⓘ, the queue arrows sit inside the screen.
This commit is contained in:
2026-09-27 02:12:54 +02:00
parent 17916b411a
commit 89e65ca331
33 changed files with 1100 additions and 282 deletions
+30
View File
@@ -712,6 +712,36 @@ describe("Home", () => {
expect(setPage).toHaveBeenCalledWith("plex");
});
// C5: what a tile's Play would continue, its menu can start over — the one way to on touch.
it("a started film's menu starts it over: the player opens from the start", async () => {
const movie = {
...ytCard("m1", "A Film"),
source: "plex",
kind: "movie",
aspect: "poster",
position_seconds: 600,
video: null,
plex: { id: "m1", type: "movie", title: "A Film", thumb: "", position_seconds: 600 },
};
discover.mockResolvedValue({
budget: "any",
hero: null,
continue_watching: [movie],
comfort: [],
});
mount();
fireEvent.click((await screen.findAllByRole("button", { name: "card.menu.more" }))[0]!);
const restart = screen
.getAllByRole("menuitem", { hidden: true })
.find((b) => b.textContent === "card.restartTitle");
fireEvent.click(restart!);
await waitFor(() =>
expect(setJumpTo).toHaveBeenCalledWith(
expect.objectContaining({ id: "m1", type: "play", fromStart: true }),
),
);
});
it("rotates the tier on each press so the button offers a different KIND of answer", async () => {
// Measured 2026-09-06: exclusion alone left it walking the resume rail and cycling nine cards,
// never once reaching the shelf. The press count travels to the server as the rotation.