feat(clip): crop a clip to a frame drawn on the picture, and give the clip panel the room it needs

An optional crop in clip mode: a Crop toggle puts a frame on the picture (free, 16:9, 9:16 or 1:1;
the whole picture stays the default). The frame is sent in fractions of the box it was drawn in,
with that box's shape; the worker fits the real picture into the box by its display aspect (sample
aspect and rotation aware), drops the part over the bars, and crops first in the filter chain. A
frame covering the whole box is no crop at all, and one off the picture is refused by name.

The crop frame is a ui primitive (CropFrame) shared with the video editor, keyboard-operable.
YouTube: the wide side panel shuts while clipping and comes back after; maximised, the clip panel
docks in the stage. Plex: the picture sits above the clip panel so the frame lies on it.
This commit is contained in:
2026-10-10 12:52:59 +02:00
parent e301632765
commit 520b0afb90
24 changed files with 1260 additions and 153 deletions
@@ -239,7 +239,7 @@ describe("backend ids have their names in both languages", () => {
expect(goneWhats).toEqual(expect.arrayContaining(["playlist", "download", "job"]));
expect(opaqueWhats).toEqual([]);
expect(youtubeReasons).toEqual(["quota", "reconnect", "unavailable", "gone"]);
expect(clipReasons).toEqual(expect.arrayContaining(["too_long", "past_end"]));
expect(clipReasons).toEqual(expect.arrayContaining(["too_long", "past_end", "bad_crop"]));
expect(crewJobs).toEqual(
expect.arrayContaining(["Director", "Creator", "Director of Photography"]),
);
+5
View File
@@ -18,12 +18,17 @@
"queued": "The clip is being made — it will be in your downloads",
"view": "Downloads",
"failed": "Couldn’t make the clip",
"crop": "Crop",
"shape": "Frame shape",
"shapeFree": "Free",
"whole": "Whole picture",
"errors": {
"bad_source": "This video can’t be clipped.",
"bad_range": "The clip’s start or end isn’t valid.",
"too_short": "A clip is at least a second long.",
"too_long": "A clip is at most 10 minutes long.",
"bad_audio": "That audio track isn’t available.",
"bad_crop": "The crop frame isn’t valid. Make it bigger, or go back to the whole picture.",
"not_found": "This item is no longer in the library.",
"media_missing": "The item’s file can’t be reached from here.",
"past_end": "The clip runs past the end of the video."
+7
View File
@@ -69,5 +69,12 @@
"done": "Copied to the clipboard",
"linkDone": "Link copied to the clipboard",
"failed": "Couldn't copy to the clipboard"
},
"cropFrame": {
"frame": "Crop frame — the arrow keys move it, with Shift further",
"nw": "Top-left corner of the crop frame — the arrow keys resize it",
"ne": "Top-right corner of the crop frame — the arrow keys resize it",
"sw": "Bottom-left corner of the crop frame — the arrow keys resize it",
"se": "Bottom-right corner of the crop frame — the arrow keys resize it"
}
}
+5
View File
@@ -18,12 +18,17 @@
"queued": "Készül a klip — a letöltéseid közé kerül",
"view": "Letöltések",
"failed": "Nem sikerült elkészíteni a klipet",
"crop": "Kivágás",
"shape": "Keret aránya",
"shapeFree": "Szabad",
"whole": "Teljes kép",
"errors": {
"bad_source": "Ebből a videóból nem lehet klipet vágni.",
"bad_range": "A klip eleje vagy vége érvénytelen.",
"too_short": "A klip legalább egy másodperc.",
"too_long": "A klip legfeljebb 10 perc lehet.",
"bad_audio": "Ez a hangsáv nem érhető el.",
"bad_crop": "A kivágókeret érvénytelen. Nagyítsd meg, vagy térj vissza a teljes képre.",
"not_found": "Ez az elem már nincs a könyvtárban.",
"media_missing": "Az elem fájlja innen nem érhető el.",
"past_end": "A klip túlnyúlik a videó végén."
+7
View File
@@ -69,5 +69,12 @@
"done": "Vágólapra másolva",
"linkDone": "Link a vágólapra másolva",
"failed": "Nem sikerült a vágólapra másolni"
},
"cropFrame": {
"frame": "Kivágókeret — a nyilakkal mozgatható, Shifttel nagyobbat lép",
"nw": "A kivágókeret bal felső sarka — a nyilakkal méretezhető",
"ne": "A kivágókeret jobb felső sarka — a nyilakkal méretezhető",
"sw": "A kivágókeret bal alsó sarka — a nyilakkal méretezhető",
"se": "A kivágókeret jobb alsó sarka — a nyilakkal méretezhető"
}
}