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
+26 -7
View File
@@ -152,7 +152,7 @@ export default function PlexBrowse() {
jumpTo.type === "show"
? { kind: "show", id: jumpTo.id }
: jumpTo.type === "play"
? { kind: "player", id: jumpTo.id }
? { kind: "player", id: jumpTo.id, fromStart: jumpTo.fromStart }
: { kind: "info", id: jumpTo.id },
// A cross-module hand-off continues the navigation that brought us here rather than adding
// to it. It also makes this effect idempotent, which matters more than it looks: under
@@ -334,6 +334,11 @@ export default function PlexBrowse() {
if (card.type === "show") sub.open({ kind: "show", id: card.id });
else sub.open({ kind: "player", id: card.id });
}
// The started card's Restart (C5): the same trip, from 0 instead of the resume point.
function onRestartCard(card: PlexCard) {
scrollRef.current = scroller()?.scrollTop ?? 0;
sub.open({ kind: "player", id: card.id, fromStart: true });
}
function onInfo(card: PlexCard) {
scrollRef.current = scroller()?.scrollTop ?? 0;
rememberInfoScroll(0); // fresh info page starts at the top
@@ -370,11 +375,11 @@ export default function PlexBrowse() {
resync: () => qc.invalidateQueries({ queryKey: qk.plexLibrary() }),
});
const actions = usePlexCardActions(
{ onOpen: onCard, onInfo, onToggleWatched: toggleWatched },
{ onOpen: onCard, onRestart: onRestartCard, onInfo, onToggleWatched: toggleWatched },
menu.open,
);
const epActions = usePlexCardActions(
{ onOpen: onCard, onToggleWatched: toggleWatched },
{ onOpen: onCard, onRestart: onRestartCard, onToggleWatched: toggleWatched },
epMenu.open,
);
// Apply a metadata filter (clicked on an info page / show hero / cast) then show the unified grid.
@@ -408,7 +413,12 @@ export default function PlexBrowse() {
<Suspense
fallback={createPortal(<div className="fixed inset-0 z-overlay bg-black" />, document.body)}
>
<PlexPlayer itemId={sub.view.id} queue={sub.view.queue} onClose={sub.back} />
<PlexPlayer
itemId={sub.view.id}
queue={sub.view.queue}
fromStart={sub.view.fromStart}
onClose={sub.back}
/>
</Suspense>
);
}
@@ -419,7 +429,9 @@ export default function PlexBrowse() {
<PlexPlaylistView
playlistId={pid}
onBack={sub.back}
onPlay={(itemRk, queue) => sub.open({ kind: "player", id: itemRk, queue })}
onPlay={(itemRk, queue, fromStart) =>
sub.open({ kind: "player", id: itemRk, queue, fromStart })
}
onInfoCard={onInfo}
/>
</Suspense>
@@ -434,6 +446,7 @@ export default function PlexBrowse() {
onBack={sub.back}
onPlay={(rk, queue) => sub.open({ kind: "player", id: rk, queue })}
onOpenCard={onCard}
onRestartCard={onRestartCard}
onInfoCard={onInfo}
/>
</Suspense>
@@ -446,7 +459,7 @@ export default function PlexBrowse() {
id={infoId}
onBack={sub.back}
onPlay={() => sub.open({ kind: "player", id: infoId })}
onPlayItem={(id) => sub.open({ kind: "player", id })}
onPlayItem={(id, fromStart) => sub.open({ kind: "player", id, fromStart })}
onFilter={applyFilter}
onOpenPerson={onOpenPerson}
/>
@@ -459,6 +472,7 @@ export default function PlexBrowse() {
name={personName}
onBack={sub.back}
onOpenCard={onCard}
onRestartCard={onRestartCard}
onInfoCard={onInfo}
onFilter={applyFilter}
/>
@@ -485,7 +499,9 @@ export default function PlexBrowse() {
showId={showId}
seasonId={seasonId}
onBack={sub.back}
onPlay={(epRk, queue) => sub.open({ kind: "player", id: epRk, queue })}
onPlay={(epRk, queue, fromStart) =>
sub.open({ kind: "player", id: epRk, queue, fromStart })
}
/>
);
}
@@ -510,6 +526,7 @@ export default function PlexBrowse() {
<PlexDiscover
scope={scope}
onOpenCard={onCard}
onRestartCard={onRestartCard}
onInfoCard={onInfo}
onGenre={(g) => applyFilter({ genres: [g] })}
onSeeAllRecent={() => sub.open({ kind: "grid" })}
@@ -685,6 +702,7 @@ export default function PlexBrowse() {
navIndex={i}
card={c}
onOpen={() => onCard(c)}
onRestart={() => onRestartCard(c)}
onToggleWatched={toggleWatched}
onAdd={actions.add}
menuProps={menu.triggerProps}
@@ -707,6 +725,7 @@ export default function PlexBrowse() {
ep={ep}
withShowTitle
onPlay={() => onCard(ep)}
onRestart={() => onRestartCard(ep)}
onToggleWatched={() => toggleWatched(ep)}
onAdd={() => epActions.add(ep)}
menuProps={epMenu.triggerProps}