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,27 @@
|
||||
// An editing timecode: to the tenth of a second, the precision a cut is marked at (the editor's
|
||||
// segment fields, the clip panel's In / Out). Hours appear only when the item reaches an hour, so a
|
||||
// short's "0:12.4" stays short and a film reads "1:02:03.4". `formatDuration` is the whole-second
|
||||
// clock for a card or a player bar; this one is for a field someone types a cut into.
|
||||
|
||||
/** `s` as `m:ss.d`, or `h:mm:ss.d` when `hours` (pass `duration >= 3600` so one item's fields agree). */
|
||||
export function formatTimecode(s: number, hours = false): string {
|
||||
if (!isFinite(s) || s < 0) s = 0;
|
||||
const tenths = Math.round(s * 10);
|
||||
const h = Math.floor(tenths / 36000);
|
||||
const m = Math.floor((tenths % 36000) / 600);
|
||||
const sec = ((tenths % 600) / 10).toFixed(1).padStart(4, "0");
|
||||
return hours || h > 0 ? `${h}:${String(m).padStart(2, "0")}:${sec}` : `${m}:${sec}`;
|
||||
}
|
||||
|
||||
/** A typed timecode back to seconds: `h:mm:ss.d`, `m:ss.d` or bare seconds. Null when it is not one. */
|
||||
export function parseTimecode(v: string): number | null {
|
||||
const parts = v.trim().split(":");
|
||||
if (parts.length > 3 || parts.some((p) => p.trim() === "")) return null;
|
||||
let total = 0;
|
||||
for (const p of parts) {
|
||||
const n = Number(p);
|
||||
if (!isFinite(n) || n < 0) return null;
|
||||
total = total * 60 + n;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
Reference in New Issue
Block a user