feat(player): clip mode - mark a range of what is playing and cut it into a download
Both players get a scissors in their control row (Plex: the control row; YouTube: the actions and the docked bar), `c`, and `[` / `]` to mark at the playhead. The clip panel lives in the player, so the watched video is the preview: an overview of the whole item and a zoomed strip around the selection whose ends are sliders (arrows step a tenth, Shift a second), Start / End fields with nudges and "to the playhead", the length, a looping Play selection, a name, Cut. Escape closes the panel before the player and the focus goes back to the scissors. A cut is a toast with a Downloads link plus the usual Ready notification. - ui/RangeTrack + RulerTicks: the shared, domain-free time track (regions, keyboard-steppable handles with a 1.5rem hit area, a playhead, a background slot, over a window of the item). The Downloads editor's cuts moved onto it, so they are keyboard-steppable now. - lib/clipRange holds the marking rules (1 s to 10 min, the server's), lib/timecode the editing timecode the editor's private tc/parseTc became. - keymap: a symbol typed through AltGr answers its bare-symbol row - on the HU layout `[` is AltGr+F, which arrives as Ctrl+Alt; letter and Mod rows still never match Ctrl+Alt. - The YouTube stage sits in a wrapper with the panel, so the maximised stage's blur escape matches at any depth. - media-scope: fields on the always-dark player chrome read white in a light theme too.
This commit is contained in:
@@ -927,3 +927,60 @@ describe("the fullscreen button", () => {
|
||||
delete (HTMLElement.prototype as { requestFullscreen?: unknown }).requestFullscreen;
|
||||
});
|
||||
});
|
||||
|
||||
// Clip mode: `[` / `]` mark at the playhead (opening the panel), the panel takes the control row's
|
||||
// place, Escape closes the panel before it would close the player, and the scissors gets the focus
|
||||
// back. Cut sends this item and the audio track that is playing.
|
||||
describe("PlexPlayer — clip mode", () => {
|
||||
const key = (k: string, init: KeyboardEventInit = {}) =>
|
||||
act(
|
||||
() =>
|
||||
void window.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { key: k, bubbles: true, cancelable: true, ...init }),
|
||||
),
|
||||
);
|
||||
|
||||
it("opens on [ with Start at the playhead, and Escape closes only the panel", async () => {
|
||||
mount({ details: { a: item("a") } });
|
||||
await playingAt("/media/a", 120);
|
||||
expect(screen.queryByTestId("clip-panel")).toBeNull();
|
||||
key("[");
|
||||
expect(screen.getByTestId("clip-panel")).toBeTruthy();
|
||||
expect(screen.getByRole("slider", { name: "clip.in" }).getAttribute("aria-valuenow")).toBe(
|
||||
"120",
|
||||
);
|
||||
// The seek bar and the control row make room for it.
|
||||
expect(screen.queryByLabelText("plex.player.playPause")).toBeNull();
|
||||
key("Escape");
|
||||
expect(screen.queryByTestId("clip-panel")).toBeNull();
|
||||
expect(screen.getByLabelText("plex.player.playPause")).toBeTruthy();
|
||||
await waitFor(() =>
|
||||
expect(document.activeElement).toBe(screen.getByRole("button", { name: "clip.open" })),
|
||||
);
|
||||
});
|
||||
|
||||
it("marks End on ] typed through AltGr (the HU layout's ])", async () => {
|
||||
mount({ details: { a: item("a") } });
|
||||
await playingAt("/media/a", 200);
|
||||
key("]", { ctrlKey: true, altKey: true, modifierAltGraph: true } as KeyboardEventInit);
|
||||
expect(screen.getByRole("slider", { name: "clip.out" }).getAttribute("aria-valuenow")).toBe(
|
||||
"200",
|
||||
);
|
||||
});
|
||||
|
||||
it("cuts this item with the audio track that is playing", async () => {
|
||||
const enqueue = vi.spyOn(api, "enqueueClip").mockResolvedValue({ id: 9 } as never);
|
||||
mount({ details: { a: item("a") } });
|
||||
await playingAt("/media/a", 60);
|
||||
fireEvent.click(screen.getByRole("button", { name: "clip.open" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "clip.cut" }));
|
||||
await waitFor(() => expect(enqueue).toHaveBeenCalled());
|
||||
expect(enqueue.mock.calls[0]![0]).toMatchObject({
|
||||
source: "plex",
|
||||
ref: "a",
|
||||
start_s: 60,
|
||||
end_s: 90,
|
||||
audio_track: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user