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:
@@ -166,6 +166,11 @@ export const SHORTCUTS = [
|
||||
{ id: "nextVideo", scope: "ytPlayer", keys: ["Shift+ArrowRight"], labelKey: "keys.video" },
|
||||
{ id: "volumeUp", scope: "ytPlayer", keys: ["ArrowUp"], labelKey: "keys.volume" },
|
||||
{ id: "volumeDown", scope: "ytPlayer", keys: ["ArrowDown"], labelKey: "keys.volume" },
|
||||
// Clip mode (the clip panel): `c` opens / closes it, `[` and `]` mark its ends at the playhead
|
||||
// (opening it when it is shut).
|
||||
{ id: "clip", scope: "ytPlayer", keys: ["c"], labelKey: "keys.clip" },
|
||||
{ id: "markIn", scope: "ytPlayer", keys: ["["], labelKey: "keys.clipMark" },
|
||||
{ id: "markOut", scope: "ytPlayer", keys: ["]"], labelKey: "keys.clipMark" },
|
||||
{
|
||||
id: "close",
|
||||
scope: "ytPlayer",
|
||||
@@ -202,6 +207,9 @@ export const SHORTCUTS = [
|
||||
{ id: "subtitle", scope: "plexPlayer", keys: ["s"], labelKey: "keys.subtitle" },
|
||||
{ id: "info", scope: "plexPlayer", keys: ["i"], labelKey: "keys.info" },
|
||||
{ id: "stats", scope: "plexPlayer", keys: ["d"], labelKey: "keys.stats" },
|
||||
{ id: "clip", scope: "plexPlayer", keys: ["c"], labelKey: "keys.clip" },
|
||||
{ id: "markIn", scope: "plexPlayer", keys: ["["], labelKey: "keys.clipMark" },
|
||||
{ id: "markOut", scope: "plexPlayer", keys: ["]"], labelKey: "keys.clipMark" },
|
||||
// Two rows, one help line: ⌫ is the plain "stop & out", Escape is the layer-aware cascade (defer to
|
||||
// anything above → cancel auto-skip → close the topmost player overlay → close). Sharing a labelKey
|
||||
// keeps them a single "⌫ Esc Stop & go back" row in the sheet.
|
||||
@@ -266,6 +274,11 @@ export function isActivationTarget(e: KeyboardEvent): boolean {
|
||||
return tag === "button" || tag === "a";
|
||||
}
|
||||
|
||||
/** One printable character that is neither a letter nor a digit (`[`, `]`, `,`). */
|
||||
function isSymbol(name: string): boolean {
|
||||
return [...name].length === 1 && !/[\p{L}\p{N}\s]/u.test(name);
|
||||
}
|
||||
|
||||
function keyMatches(e: KeyboardEvent, name: string): boolean {
|
||||
if (name === "Space") return e.key === " " || e.code === "Space";
|
||||
if (name.length === 1) return e.key.toLowerCase() === name.toLowerCase();
|
||||
@@ -280,6 +293,10 @@ export function matches(e: KeyboardEvent, chord: string): boolean {
|
||||
const parts = chord.split("+");
|
||||
const key = parts[parts.length - 1] ?? "";
|
||||
const mods = parts.slice(0, -1).map((m) => m.toLowerCase());
|
||||
// A symbol the layout types through AltGr (the HU `[` is AltGr+F, Ctrl+Alt on Windows) is that
|
||||
// symbol: a bare-symbol row answers it. A letter or a Mod row never does — AltGr+K is not Ctrl+K.
|
||||
if (mods.length === 0 && isSymbol(key) && e.getModifierState("AltGraph"))
|
||||
return keyMatches(e, key);
|
||||
if ((e.ctrlKey || e.metaKey) !== mods.includes("mod")) return false;
|
||||
if (e.shiftKey !== mods.includes("shift")) return false;
|
||||
if (e.altKey !== mods.includes("alt")) return false;
|
||||
|
||||
Reference in New Issue
Block a user