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:
@@ -101,3 +101,26 @@ describe("VideoEditor — a split export that fails part-way", () => {
|
||||
expect(enqueue.mock.calls[2]![0].display_name).toBe("Talk (2/2)");
|
||||
});
|
||||
});
|
||||
|
||||
// The cuts sit on the shared time track: each is a slider the keyboard steps, and a step stops a
|
||||
// tenth short of the next cut, so no segment ever closes up.
|
||||
describe("VideoEditor — a cut on the keyboard", () => {
|
||||
it("steps a focused cut with the arrows and stops it before its neighbours", () => {
|
||||
vi.spyOn(api, "downloadStoryboard").mockResolvedValue({ available: false } as never);
|
||||
render(
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<VideoEditor job={job} onClose={() => {}} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
const video = document.querySelector("video")!;
|
||||
Object.defineProperty(video, "currentTime", { value: 50, configurable: true, writable: true });
|
||||
fireEvent.timeUpdate(video);
|
||||
fireEvent.click(screen.getByText("editor.splitHere"));
|
||||
const cut = screen.getByRole("slider", { name: "editor.cutN" });
|
||||
expect(cut.getAttribute("aria-valuenow")).toBe("50");
|
||||
fireEvent.keyDown(cut, { key: "ArrowRight", shiftKey: true });
|
||||
expect(cut.getAttribute("aria-valuenow")).toBe("51");
|
||||
fireEvent.keyDown(cut, { key: "End" });
|
||||
expect(cut.getAttribute("aria-valuenow")).toBe("99.9");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user