fix(clip): clip mode keeps its range, its focus and its keys through the player's own moves
- A mark on the very end seeks a moment short of it; while a clip is marked the end of the item neither closes the player nor steps the queue, and Plex's auto-skip waits. - Plex: Escape and Back peel what is over the picture (info, up-next, the countdown) before the clip; Back under a dialog is the dialog's; no range opens while the length is unknown. - YouTube: the focus goes back to the scissors the user can see, else the player; the panel keeps its name, Loop and focus when the view is maximised or restored under it. - A typed time stays in its field until taken, then shows where it landed; unreadable stays marked invalid; a decimal comma is read (ui/TimecodeField, the editor's fields too). - RangeTrack: a drag is its pointer's, a cancelled drag reports final, an adornment sits beside the slider, a caller-chosen focus ring (amber on the clip strip). CropFrame: a drag is its pointer's. - The editor's remove-cut is a real button at the cut's foot; a segment's keep toggle sits over the cut's hit area.
This commit is contained in:
@@ -24,6 +24,7 @@ vi.mock("react-i18next", async (importOriginal) => {
|
||||
import PlexPlayer from "./PlexPlayer";
|
||||
import { api, type PlexItemDetail } from "../lib/api";
|
||||
import { ACTIVE_ACCOUNT_KEY } from "../lib/activeAccount";
|
||||
import { pushLayer, removeLayer } from "../lib/layerStack";
|
||||
|
||||
type Media = HTMLMediaElement & { _t?: number; _paused?: boolean };
|
||||
|
||||
@@ -128,6 +129,7 @@ function mount(opts: {
|
||||
queue?: string[];
|
||||
first?: string;
|
||||
fromStart?: boolean;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
vi.spyOn(api, "plexItem").mockImplementation((id: string) => Promise.resolve(opts.details[id]!));
|
||||
const session = vi
|
||||
@@ -139,7 +141,12 @@ function mount(opts: {
|
||||
const first = opts.first ?? opts.queue?.[0] ?? Object.keys(opts.details)[0]!;
|
||||
render(
|
||||
<QueryClientProvider client={qc}>
|
||||
<PlexPlayer itemId={first} onClose={() => {}} queue={opts.queue} fromStart={opts.fromStart} />
|
||||
<PlexPlayer
|
||||
itemId={first}
|
||||
onClose={opts.onClose ?? (() => {})}
|
||||
queue={opts.queue}
|
||||
fromStart={opts.fromStart}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
return { session };
|
||||
@@ -1023,4 +1030,71 @@ describe("PlexPlayer — clip mode", () => {
|
||||
else delete (proto as unknown as Record<string, unknown>)[k];
|
||||
}
|
||||
});
|
||||
|
||||
const prefs = (p: object) =>
|
||||
localStorage.setItem("siftlode.plexPlayerPrefs.7", JSON.stringify({ subLang: "en", ...p }));
|
||||
const infoOverlay = () => document.querySelector("[class*='bg-black/70']");
|
||||
|
||||
it("Escape closes what is over the picture first, then the crop and the panel", async () => {
|
||||
const full = { ...item("a"), cast: [], genres: [], directors: [], collections: [] };
|
||||
mount({ details: { a: full as PlexItemDetail } });
|
||||
await playingAt("/media/a", 120);
|
||||
key("c");
|
||||
key("i");
|
||||
expect(infoOverlay()).toBeTruthy();
|
||||
key("Escape");
|
||||
expect(infoOverlay()).toBeNull();
|
||||
expect(screen.getByTestId("clip-panel")).toBeTruthy();
|
||||
key("Escape");
|
||||
expect(screen.queryByTestId("clip-panel")).toBeNull();
|
||||
});
|
||||
|
||||
it("⌫ under a dialog over the player is the dialog's, not the clip's", async () => {
|
||||
mount({ details: { a: item("a") } });
|
||||
await playingAt("/media/a", 120);
|
||||
key("c");
|
||||
const layer = pushLayer();
|
||||
try {
|
||||
key("Backspace");
|
||||
expect(screen.getByTestId("clip-panel")).toBeTruthy();
|
||||
} finally {
|
||||
removeLayer(layer);
|
||||
}
|
||||
key("Backspace");
|
||||
expect(screen.queryByTestId("clip-panel")).toBeNull();
|
||||
});
|
||||
|
||||
it("opens no range while the item's length is unknown", async () => {
|
||||
mount({
|
||||
details: { a: { ...item("a"), duration_seconds: null } as unknown as PlexItemDetail },
|
||||
});
|
||||
await playingAt("/media/a", 120);
|
||||
for (const k of ["c", "[", "]"]) key(k);
|
||||
expect(screen.queryByTestId("clip-panel")).toBeNull();
|
||||
});
|
||||
|
||||
it("an item played to its end while a clip is marked stays, with the clip", async () => {
|
||||
const onClose = vi.fn();
|
||||
mount({ details: { a: item("a") }, onClose });
|
||||
await playingAt("/media/a", 2990);
|
||||
key("c");
|
||||
act(() => void video().dispatchEvent(new Event("ended")));
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId("clip-panel")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not auto-skip while a clip is marked: the skip would carry the player off it", async () => {
|
||||
prefs({ autoSkipCredits: true, autoSkipDelay: 0 });
|
||||
const withCredits = {
|
||||
...item("a", 0, "b"),
|
||||
markers: [{ type: "credits", start_s: 2900, end_s: 2995, final: true }],
|
||||
} as unknown as PlexItemDetail;
|
||||
const { session } = mount({ details: { a: withCredits, b: item("b") }, queue: ["a", "b"] });
|
||||
await playingAt("/media/a", 2800);
|
||||
key("c");
|
||||
await playingAt("/media/a", 2910);
|
||||
await act(() => new Promise((r) => setTimeout(r, 50)));
|
||||
expect(lastStart(session)?.[0]).toBe("a");
|
||||
expect(screen.getByTestId("clip-panel")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user