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
+33 -3
View File
@@ -11,7 +11,8 @@ import { plexAddLabelKey } from "./usePlexCardActions";
import { IconButton } from "./ui/IconButton";
import { MoreButton } from "./ui/MoreButton";
import { Pressable } from "./ui/Pressable";
import { HOVER_YIELD } from "./ui/reveal";
import { HOVER_REVEAL, HOVER_YIELD } from "./ui/reveal";
import { MediaResumeButtons } from "./MediaResumeButtons";
import {
MEDIA_BADGE,
MEDIA_BAR,
@@ -173,12 +174,15 @@ function CardTitle({
}
/** A Plex card's picture: the whole box is the card's open/play button (Enter opens it from the
* keyboard walk), with the hover row docked beside it rather than inside it. */
* keyboard walk), with the hover row docked beside it rather than inside it. A started item shows the
* video card's Continue + Restart pair instead of the Play (C5); like the hover row, the pair is a
* sibling laid over the picture, never inside its button. */
function PlexPicture({
aspect,
thumb,
label,
onOpen,
onRestart,
plays,
small = false,
pct = 0,
@@ -190,6 +194,8 @@ function PlexPicture({
/** The button's name: the picture has no text of its own. */
label: string;
onOpen: () => void;
/** Play from the start, where the surface can (the player's `fromStart`). */
onRestart?: () => void;
/** The click plays (a movie, an episode) — only then does the hover draw a Play. */
plays: boolean;
/** A shelf's short landscape picture: the smaller Play, clear of the hover row under it. */
@@ -199,6 +205,8 @@ function PlexPicture({
/** The badges printed on the picture. */
children?: ReactNode;
}) {
// The pct is the resume point's: a watched or untouched item has none, and plays from its start.
const started = plays && pct > 0 && !!onRestart;
return (
<div className={mediaPictureBox(aspect)}>
<div
@@ -217,9 +225,23 @@ function PlexPicture({
>
<img src={thumb} alt="" loading="lazy" decoding="async" className={MEDIA_IMG} />
{children}
<div className={MEDIA_HOVER_LAYER}>{plays && <MediaPlayGlyph small={small} />}</div>
<div className={MEDIA_HOVER_LAYER}>
{plays && !started && <MediaPlayGlyph small={small} />}
</div>
<MediaProgress pct={pct} />
</div>
{started && (
<div
className={clsx(
HOVER_REVEAL,
"pointer-events-none absolute inset-0 z-base flex items-center justify-center gap-2 *:pointer-events-auto",
// A poster is too narrow for the pair side by side (150px); it stacks, as in the lab.
aspect === "poster" && "flex-col",
)}
>
<MediaResumeButtons small={small} onContinue={onOpen} onRestart={onRestart} />
</div>
)}
{bar}
</div>
);
@@ -243,9 +265,12 @@ export function PlexPosterCard({
shelf = false,
next,
onInfo,
onRestart,
}: {
card: PlexCard;
onOpen: () => void;
/** Play the item (or the `next` episode) from its start, not its resume point (C5). */
onRestart?: () => void;
onToggleWatched?: (c: PlexCard) => void;
/** Add a movie or an episode to a playlist (the list's `usePlexCardActions().add`). */
onAdd?: (c: PlexCard) => void;
@@ -316,6 +341,7 @@ export function PlexPosterCard({
thumb={(wide && card.type !== "episode" && card.art) || card.thumb}
label={card.title}
onOpen={onOpen}
onRestart={onRestart}
plays={isPlayable}
small={shelf && landscape}
pct={pct}
@@ -591,6 +617,7 @@ export function RelatedStrip({
export function EpisodeCard({
ep,
onPlay,
onRestart,
onAdd,
onToggleWatched,
withShowTitle,
@@ -601,6 +628,8 @@ export function EpisodeCard({
}: {
ep: PlexCard;
onPlay: () => void;
/** Play the episode from its start, not its resume point (C5). */
onRestart?: () => void;
onAdd?: () => void;
onToggleWatched?: () => void;
withShowTitle?: boolean;
@@ -628,6 +657,7 @@ export function EpisodeCard({
thumb={ep.thumb}
label={ep.title}
onOpen={onPlay}
onRestart={onRestart}
plays
pct={pct}
bar={