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 -4
View File
@@ -10,6 +10,7 @@ import {
PinOff,
Play,
RotateCcw,
SkipBack,
Star,
} from "lucide-react";
import { qk } from "../lib/queryKeys";
@@ -51,7 +52,8 @@ import { SURFACE } from "./ui/surfaces";
import clsx from "clsx";
import { IconButton } from "./ui/IconButton";
import { MoreButton } from "./ui/MoreButton";
import { HOVER_YIELD } from "./ui/reveal";
import { HOVER_REVEAL, HOVER_YIELD } from "./ui/reveal";
import { MediaResumeButtons } from "./MediaResumeButtons";
import { MediaCard, MediaPlayGlyph } from "./ui/MediaCard";
import {
MEDIA_BAR,
@@ -171,6 +173,17 @@ function useHomeCardMenu({
disabled: !(target.next ?? target).video && !(target.next ?? target).plex,
onSelect: () => onPlay(target),
},
// What the Play row would continue can also start over (C5), with the card menus' words.
...((target.next ?? target).position_seconds > 0
? [
{
id: "restart",
label: t("card.restartTitle"),
icon: <SkipBack className="w-4 h-4" />,
onSelect: () => onPlay(target, 0),
},
]
: []),
...(target.video
? videoActions
.items(target.video, menu.anchor, { pin: false })
@@ -344,6 +357,7 @@ function Rail({
shelf
navIndex={i}
onOpen={() => onPlay(c)}
onRestart={() => onPlay(c, 0)}
onToggleWatched={onToggleWatched}
onAdd={plexAdd}
next={c.next?.plex}
@@ -569,10 +583,21 @@ function Hero({
/>
</span>
) : null}
<div className={MEDIA_HOVER_LAYER}>
<MediaPlayGlyph />
</div>
<div className={MEDIA_HOVER_LAYER}>{!started && <MediaPlayGlyph />}</div>
</Pressable>
{/* A started hero continues on a click, and offers the start as a card does (C5): the pair
is a sibling over the picture, never inside its button. */}
{started && (
<div
className={clsx(
HOVER_REVEAL,
"pointer-events-none absolute inset-0 z-base flex items-center justify-center gap-2 *:pointer-events-auto",
card.aspect === "poster" && "flex-col",
)}
>
<MediaResumeButtons onContinue={() => onPlay(card)} onRestart={() => onPlay(card, 0)} />
</div>
)}
{/* The card's own hover row, as on every card: a sibling of the picture's button. */}
{card.video ? (
<div className={MEDIA_BAR}>
@@ -851,6 +876,7 @@ export default function Home() {
setJumpTo({
id: target.plex.id,
type: target.plex.type === "show" ? "show" : "play",
fromStart: startAt === 0,
replace: true,
});
setPage("plex");