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:
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatTimecode, parseTimecode } from "./timecode";
|
||||
|
||||
describe("formatTimecode", () => {
|
||||
it("is m:ss.d below an hour and h:mm:ss.d from one", () => {
|
||||
expect(formatTimecode(12.44)).toBe("0:12.4");
|
||||
expect(formatTimecode(75)).toBe("1:15.0");
|
||||
expect(formatTimecode(3723.4)).toBe("1:02:03.4");
|
||||
});
|
||||
|
||||
it("shows hours when the item reaches one, so an item's fields agree", () => {
|
||||
expect(formatTimecode(65, true)).toBe("0:01:05.0");
|
||||
});
|
||||
|
||||
it("carries a rounded tenth into the next second, never a 60.0", () => {
|
||||
expect(formatTimecode(59.96)).toBe("1:00.0");
|
||||
});
|
||||
|
||||
it("reads a bad clock as zero", () => {
|
||||
expect(formatTimecode(NaN)).toBe("0:00.0");
|
||||
expect(formatTimecode(-3)).toBe("0:00.0");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseTimecode", () => {
|
||||
it("reads every shape it writes, and bare seconds", () => {
|
||||
expect(parseTimecode("0:12.4")).toBeCloseTo(12.4);
|
||||
expect(parseTimecode("1:02:03.4")).toBeCloseTo(3723.4);
|
||||
expect(parseTimecode(" 90.5 ")).toBeCloseTo(90.5);
|
||||
});
|
||||
|
||||
it("is null for what is not a timecode", () => {
|
||||
for (const v of ["", "abc", "1::2", "1:2:3:4", "-5", "1:-2"])
|
||||
expect(parseTimecode(v)).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user