fix(player): marks give way to newer lists, Watch later items mark, and auto-advance has its own glyphs
A session's marks are weighed per field against when each page of a never-refetched list was read: a page read before the mark (a Show more / Load more on its way) takes it, one read after keeps the server's word. The live search lays them over its rows too, not only the related list. Items of the built-in Watch later list ARE the bookmark, so adding, removing and moving them go through features/video/videoWrites and mark the video; the contract test covers those writes and catches a write that is named without being called on the spot. The player's watched toggle follows a mark confirmed elsewhere since it opened (the related row's menu, a notice's Undo); Restart on the related row after a description link goes back to it. The file viewer tells the help sheet its keys wait under a layer; refusalReason replaces the last two open-coded reads. Auto-advance wears list glyphs of its own (the skip arrows are Previous / Next beside it), and the auto-advance and loop cycles are `lit`, not toggles: no aria-pressed, the name says the mode. Both places render from one list of controls, so the docked tooltips name the mode too. The English cap message and admin hint quote "Show more"; the hint says refunds are limited.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import type { CSSProperties, Ref } from "react";
|
||||
import type { CSSProperties, ReactNode, Ref } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Repeat, Repeat1, Shuffle, SkipBack, SkipForward } from "lucide-react";
|
||||
import { ListEnd, ListStart, Repeat, Repeat1, Shuffle, SkipBack, SkipForward } from "lucide-react";
|
||||
import type { VideoDetail } from "../../lib/api";
|
||||
import type { AutoMode, LoopMode } from "../../lib/playerModes";
|
||||
import { Button } from "../../components/ui/Button";
|
||||
@@ -13,13 +13,16 @@ import { renderDescription } from "./descriptionLinks";
|
||||
// the sibling of plexPlayerControls.tsx. No state or effects here; all behaviour comes in via props.
|
||||
// The auto-advance / loop modes it renders are defined in `lib/playerModes`.
|
||||
|
||||
/** The queue transport cluster — prev / counter / next, then the auto-advance and loop mode toggles.
|
||||
* Rendered in two places with identical logic: the modal's queue bar (`variant:"modal"`, normal glass
|
||||
* theme, glyphs only so the whole cluster fits one line of the side panel) and the big-view docked
|
||||
* control bar (`variant:"docked"`, white-on-dark, labels shown from a breakpoint up). Only the theme +
|
||||
* label visibility differ; the behaviour is one, so a mode/icon change lands once. Every control keeps
|
||||
* its full name (with the shortcut) as its tooltip and accessible name, so a glyph-only one still says
|
||||
* what it does and which mode it is in. The wrapper (fragment vs. bordered row) stays at the call site. */
|
||||
/** The queue transport cluster — prev / counter / next, then the auto-advance and loop modes.
|
||||
* Rendered in two places from ONE list of controls: the modal's queue bar (`variant:"modal"`, normal
|
||||
* glass theme, glyphs only so the whole cluster fits one line of the side panel) and the big-view
|
||||
* docked control bar (`variant:"docked"`, white-on-dark, labels shown from a breakpoint up). What a
|
||||
* control does, its glyph and its tooltip are said once; only the primitive and the label's
|
||||
* visibility differ. Every control's tooltip carries its full name (and the shortcut, where it has
|
||||
* one), so a glyph-only one still says what it does and which mode it is in. The modes are cycles,
|
||||
* not switches: `lit` when not off, and the name says which. Auto-advance wears list glyphs of its
|
||||
* own — the skip arrows are Previous / Next, beside it (DESIGN.md "A glyph means one thing"). The
|
||||
* wrapper (fragment vs. bordered row) stays at the call site. */
|
||||
export function PlayerTransportControls({
|
||||
variant,
|
||||
index,
|
||||
@@ -47,102 +50,101 @@ export function PlayerTransportControls({
|
||||
const divider = docked ? "h-4 w-px bg-white/20" : "w-px h-4 bg-border";
|
||||
const autoName = `${t("player.autoAdvance.label")}: ${t(`player.autoAdvance.${autoMode}`)}`;
|
||||
const loopName = `${t("player.loop.label")}: ${t(`player.loop.${loopMode}`)}`;
|
||||
const autoIcon =
|
||||
autoMode === "prev" ? <SkipBack /> : autoMode === "random" ? <Shuffle /> : <SkipForward />;
|
||||
const loopIcon = loopMode === "one" ? <Repeat1 /> : <Repeat />;
|
||||
const count = (
|
||||
<span className={counter}>
|
||||
{index + 1} / {queueLength}
|
||||
</span>
|
||||
);
|
||||
if (!docked)
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
onClick={onPrev}
|
||||
disabled={index === 0}
|
||||
label={t("player.previous")}
|
||||
title={`${t("player.previous")} · Shift+←`}
|
||||
>
|
||||
<SkipBack />
|
||||
</IconButton>
|
||||
{count}
|
||||
<IconButton
|
||||
onClick={onNext}
|
||||
disabled={index === queueLength - 1}
|
||||
label={t("player.next")}
|
||||
title={`${t("player.next")} · Shift+→`}
|
||||
>
|
||||
<SkipForward />
|
||||
</IconButton>
|
||||
<span className={divider} />
|
||||
<IconButton
|
||||
pressed={autoMode !== "off"}
|
||||
onClick={onCycleAuto}
|
||||
label={autoName}
|
||||
title={`${autoName} · ${t("player.autoAdvance.hint")}`}
|
||||
>
|
||||
{autoIcon}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
pressed={loopMode !== "off"}
|
||||
onClick={onCycleLoop}
|
||||
label={loopName}
|
||||
title={`${loopName} · ${t("player.loop.hint")}`}
|
||||
>
|
||||
{loopIcon}
|
||||
</IconButton>
|
||||
</>
|
||||
type Control = {
|
||||
key: string;
|
||||
glyph: ReactNode;
|
||||
name: string;
|
||||
title: string;
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
lit?: boolean;
|
||||
/** From which width the docked bar shows the name beside the glyph. */
|
||||
labelFrom: "sm" | "md";
|
||||
/** The glyph after the name (Next points the way it goes). */
|
||||
trailing?: boolean;
|
||||
};
|
||||
const step: Control[] = [
|
||||
{
|
||||
key: "prev",
|
||||
glyph: <SkipBack />,
|
||||
name: t("player.previous"),
|
||||
title: `${t("player.previous")} · Shift+←`,
|
||||
onClick: onPrev,
|
||||
disabled: index === 0,
|
||||
labelFrom: "sm",
|
||||
},
|
||||
{
|
||||
key: "next",
|
||||
glyph: <SkipForward />,
|
||||
name: t("player.next"),
|
||||
title: `${t("player.next")} · Shift+→`,
|
||||
onClick: onNext,
|
||||
disabled: index === queueLength - 1,
|
||||
labelFrom: "sm",
|
||||
trailing: true,
|
||||
},
|
||||
];
|
||||
// Persistent playback settings — cycle on click, saved to your account.
|
||||
const modes: Control[] = [
|
||||
{
|
||||
key: "auto",
|
||||
glyph:
|
||||
autoMode === "prev" ? <ListStart /> : autoMode === "random" ? <Shuffle /> : <ListEnd />,
|
||||
name: autoName,
|
||||
title: `${autoName} · ${t("player.autoAdvance.hint")}`,
|
||||
onClick: onCycleAuto,
|
||||
lit: autoMode !== "off",
|
||||
labelFrom: "md",
|
||||
},
|
||||
{
|
||||
key: "loop",
|
||||
glyph: loopMode === "one" ? <Repeat1 /> : <Repeat />,
|
||||
name: loopName,
|
||||
title: `${loopName} · ${t("player.loop.hint")}`,
|
||||
onClick: onCycleLoop,
|
||||
lit: loopMode !== "off",
|
||||
labelFrom: "md",
|
||||
},
|
||||
];
|
||||
const render = (c: Control) =>
|
||||
docked ? (
|
||||
<Button
|
||||
key={c.key}
|
||||
variant="overlay"
|
||||
size="sm"
|
||||
icon={c.trailing ? undefined : c.glyph}
|
||||
onClick={c.onClick}
|
||||
disabled={c.disabled}
|
||||
lit={c.lit}
|
||||
label={c.name}
|
||||
title={c.title}
|
||||
>
|
||||
<span className={c.labelFrom === "sm" ? "hidden sm:inline" : "hidden md:inline"}>
|
||||
{c.name}
|
||||
</span>
|
||||
{c.trailing && c.glyph}
|
||||
</Button>
|
||||
) : (
|
||||
<IconButton
|
||||
key={c.key}
|
||||
onClick={c.onClick}
|
||||
disabled={c.disabled}
|
||||
lit={c.lit}
|
||||
label={c.name}
|
||||
title={c.title}
|
||||
>
|
||||
{c.glyph}
|
||||
</IconButton>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="overlay"
|
||||
size="sm"
|
||||
icon={<SkipBack />}
|
||||
onClick={onPrev}
|
||||
disabled={index === 0}
|
||||
label={t("player.previous")}
|
||||
title={`${t("player.previous")} · Shift+←`}
|
||||
>
|
||||
<span className="hidden sm:inline">{t("player.previous")}</span>
|
||||
</Button>
|
||||
{count}
|
||||
<Button
|
||||
variant="overlay"
|
||||
size="sm"
|
||||
onClick={onNext}
|
||||
disabled={index === queueLength - 1}
|
||||
label={t("player.next")}
|
||||
title={`${t("player.next")} · Shift+→`}
|
||||
>
|
||||
<span className="hidden sm:inline">{t("player.next")}</span>
|
||||
<SkipForward />
|
||||
</Button>
|
||||
{render(step[0]!)}
|
||||
<span className={counter}>
|
||||
{index + 1} / {queueLength}
|
||||
</span>
|
||||
{render(step[1]!)}
|
||||
<span className={divider} />
|
||||
{/* Persistent playback settings — cycle on click, saved to your account. */}
|
||||
<Button
|
||||
variant="overlay"
|
||||
size="sm"
|
||||
pressed={autoMode !== "off"}
|
||||
icon={autoIcon}
|
||||
onClick={onCycleAuto}
|
||||
label={autoName}
|
||||
title={t("player.autoAdvance.hint")}
|
||||
>
|
||||
<span className="hidden md:inline">{autoName}</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="overlay"
|
||||
size="sm"
|
||||
pressed={loopMode !== "off"}
|
||||
icon={loopIcon}
|
||||
onClick={onCycleLoop}
|
||||
label={loopName}
|
||||
title={t("player.loop.hint")}
|
||||
>
|
||||
<span className="hidden md:inline">{loopName}</span>
|
||||
</Button>
|
||||
{modes.map(render)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user