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,55 @@
|
||||
// The range an ad-hoc clip is cut from, marked in a player: the rules the clip panel's handles, its
|
||||
// In / Out fields and the players' `[` / `]` keys all go by, so every way of marking keeps a range the
|
||||
// server takes (backend downloads/clip.py: at least MIN_CLIP_S, at most MAX_CLIP_S — the owner's cap).
|
||||
|
||||
const MIN_CLIP_S = 1;
|
||||
export const MAX_CLIP_S = 600;
|
||||
/** What a fresh mark spans before the second end is set: a moment, not the cap. */
|
||||
export const DEFAULT_CLIP_S = 30;
|
||||
|
||||
export interface ClipRange {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
const clamp = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, v));
|
||||
|
||||
/** A range opened at `t`: from there, DEFAULT_CLIP_S long (shorter at the item's end). */
|
||||
export function openAt(t: number, duration: number): ClipRange {
|
||||
const start = clamp(t, 0, Math.max(0, duration - MIN_CLIP_S));
|
||||
return { start, end: Math.min(duration, start + DEFAULT_CLIP_S) };
|
||||
}
|
||||
|
||||
/** In moved to `t`. Out stays where it is while the range stays one the server takes; otherwise it
|
||||
* follows (a fresh DEFAULT_CLIP_S after an In past it, the cap after an In too far before it). */
|
||||
export function markIn(r: ClipRange, t: number, duration: number): ClipRange {
|
||||
const start = clamp(t, 0, Math.max(0, duration - MIN_CLIP_S));
|
||||
let end = r.end;
|
||||
if (end - start < MIN_CLIP_S) end = Math.min(duration, start + DEFAULT_CLIP_S);
|
||||
else if (end - start > MAX_CLIP_S) end = start + MAX_CLIP_S;
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
/** Out moved to `t`; In follows the same way when the range would stop being one. */
|
||||
export function markOut(r: ClipRange, t: number, duration: number): ClipRange {
|
||||
const end = clamp(t, Math.min(MIN_CLIP_S, duration), duration);
|
||||
let start = r.start;
|
||||
if (end - start < MIN_CLIP_S) start = Math.max(0, end - DEFAULT_CLIP_S);
|
||||
else if (end - start > MAX_CLIP_S) start = end - MAX_CLIP_S;
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
/** Where each end may be dragged to without the other moving. */
|
||||
export function handleBounds(r: ClipRange, duration: number) {
|
||||
return {
|
||||
in: { min: Math.max(0, r.end - MAX_CLIP_S), max: r.end - MIN_CLIP_S },
|
||||
out: { min: r.start + MIN_CLIP_S, max: Math.min(duration, r.start + MAX_CLIP_S) },
|
||||
};
|
||||
}
|
||||
|
||||
/** The zoomed strip's window: the range with a margin either side (a quarter of it, at least 5 s),
|
||||
* so both ends sit inside with room to move them out. */
|
||||
export function zoomWindow(r: ClipRange, duration: number): { from: number; to: number } {
|
||||
const pad = Math.max(5, (r.end - r.start) * 0.25);
|
||||
return { from: Math.max(0, r.start - pad), to: Math.min(duration, r.end + pad) };
|
||||
}
|
||||
Reference in New Issue
Block a user