fix(plex): a card offers Restart by the menu's own "started" rule

The picture read "started" off the rounded progress %, so a resume point under 1 % showed Play
while the card's menu offered "from the start". Both use `plexStarted` now.
This commit is contained in:
2026-09-27 14:38:27 +02:00
parent a7d41eb5f3
commit efa4aeeeda
2 changed files with 24 additions and 5 deletions
@@ -208,6 +208,19 @@ describe("a started Plex card", () => {
expect(container.querySelector(".flex-col.reveal-hover")).toBeNull();
});
// E28 review D4: the card read "started" off the rounded progress %, the menu off the position —
// 20 s into a 3 h film is 0 % here and started there, so the card offered no Restart the menu had.
it("a resume point under 1 % still offers the Restart, as the card's menu does", () => {
render(
<PlexPosterCard
card={{ ...movie("in_progress"), position_seconds: 20, duration_seconds: 10800 }}
onOpen={vi.fn()}
onRestart={vi.fn()}
/>,
);
expect(screen.getByRole("button", { name: "card.restart" })).toBeTruthy();
});
it("a not-started or watched card keeps the one Play", () => {
render(<PlexPosterCard card={movie("watched")} onOpen={vi.fn()} onRestart={vi.fn()} />);
expect(screen.queryByRole("button", { name: "card.restart" })).toBeNull();
+11 -5
View File
@@ -7,7 +7,7 @@ import { Carousel } from "./ui/Carousel";
import type { PlexCard, PlexCastMember, PlexFilters, PlexSeasonDetail } from "../lib/api";
import { navItemAttrs } from "../lib/listNav";
import { endsTextSelection, movedPastSlop } from "../lib/textSelection";
import { plexAddLabelKey } from "./usePlexCardActions";
import { plexAddLabelKey, plexStarted } from "./usePlexCardActions";
import { IconButton } from "./ui/IconButton";
import { MoreButton } from "./ui/MoreButton";
import { Pressable } from "./ui/Pressable";
@@ -186,6 +186,7 @@ function PlexPicture({
plays,
small = false,
pct = 0,
started = false,
bar,
children,
}: {
@@ -201,23 +202,26 @@ function PlexPicture({
/** A shelf's short landscape picture: the smaller Play, clear of the hover row under it. */
small?: boolean;
pct?: number;
/** Has a resume point (`plexStarted`, the card menu's own rule): Continue + Restart, not Play. */
started?: boolean;
bar: ReactNode;
/** 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;
// Not read off `pct`: that is rounded, so 20 s into a 3 h film was 0 % and "not started" here while
// the menu offered "from the start" (E28 review D4). One rule, `plexStarted`, for both.
const resumable = plays && started && !!onRestart;
return (
<div className={mediaPictureBox(aspect)}>
<Pressable onClick={onOpen} aria-label={label} data-nav-open="" className={MEDIA_PICTURE}>
<img src={thumb} alt="" loading="lazy" decoding="async" className={MEDIA_IMG} />
{children}
<div className={MEDIA_HOVER_LAYER}>
{plays && !started && <MediaPlayGlyph small={small} />}
{plays && !resumable && <MediaPlayGlyph small={small} />}
</div>
<MediaProgress pct={pct} />
</Pressable>
{started && (
{resumable && (
<div
className={clsx(
HOVER_REVEAL,
@@ -332,6 +336,7 @@ export function PlexPosterCard({
plays={isPlayable}
small={shelf && landscape}
pct={pct}
started={plexStarted(acted)}
bar={
<HoverBar
watched={actedWatched}
@@ -647,6 +652,7 @@ export function EpisodeCard({
onRestart={onRestart}
plays
pct={pct}
started={plexStarted(ep)}
bar={
<HoverBar
watched={watched}