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.
66 lines
2.3 KiB
TypeScript
66 lines
2.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
DEFAULT_CLIP_S,
|
|
MAX_CLIP_S,
|
|
handleBounds,
|
|
markIn,
|
|
markOut,
|
|
openAt,
|
|
zoomWindow,
|
|
} from "./clipRange";
|
|
|
|
const D = 3600;
|
|
|
|
describe("clipRange", () => {
|
|
it("opens a default-length range at the playhead, shorter at the item's end", () => {
|
|
expect(openAt(100, D)).toEqual({ start: 100, end: 100 + DEFAULT_CLIP_S });
|
|
expect(openAt(3590, D)).toEqual({ start: 3590, end: 3600 });
|
|
expect(openAt(3600, D)).toEqual({ start: 3599, end: 3600 });
|
|
});
|
|
|
|
it("moves In alone while the range stays one the server takes", () => {
|
|
expect(markIn({ start: 100, end: 130 }, 90, D)).toEqual({ start: 90, end: 130 });
|
|
});
|
|
|
|
it("carries Out along when In passes it, and keeps the cap when In goes too far back", () => {
|
|
expect(markIn({ start: 100, end: 130 }, 200, D)).toEqual({ start: 200, end: 230 });
|
|
expect(markIn({ start: 1000, end: 1030 }, 100, D)).toEqual({
|
|
start: 100,
|
|
end: 100 + MAX_CLIP_S,
|
|
});
|
|
});
|
|
|
|
it("carries In along when Out goes before it or too far ahead", () => {
|
|
expect(markOut({ start: 100, end: 130 }, 150, D)).toEqual({ start: 100, end: 150 });
|
|
expect(markOut({ start: 100, end: 130 }, 50, D)).toEqual({ start: 20, end: 50 });
|
|
expect(markOut({ start: 100, end: 130 }, 1000, D)).toEqual({ start: 400, end: 1000 });
|
|
});
|
|
|
|
it("every marked range is at least a second and at most the cap", () => {
|
|
for (const [r, t] of [
|
|
[{ start: 0, end: 30 }, 0],
|
|
[{ start: 0, end: 30 }, 3600],
|
|
[{ start: 3000, end: 3600 }, 0],
|
|
] as const) {
|
|
for (const next of [markIn(r, t, D), markOut(r, t, D)]) {
|
|
expect(next.end - next.start).toBeGreaterThanOrEqual(1);
|
|
expect(next.end - next.start).toBeLessThanOrEqual(MAX_CLIP_S);
|
|
expect(next.start).toBeGreaterThanOrEqual(0);
|
|
expect(next.end).toBeLessThanOrEqual(D);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("bounds each handle so a drag never breaks the range", () => {
|
|
expect(handleBounds({ start: 100, end: 130 }, D)).toEqual({
|
|
in: { min: 0, max: 129 },
|
|
out: { min: 101, max: 700 },
|
|
});
|
|
});
|
|
|
|
it("zooms to the range with room either side, inside the item", () => {
|
|
expect(zoomWindow({ start: 100, end: 140 }, D)).toEqual({ from: 90, to: 150 });
|
|
expect(zoomWindow({ start: 2, end: 10 }, D)).toEqual({ from: 0, to: 15 });
|
|
});
|
|
});
|