@pixerate/editor-react 0.56.0 → 0.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -35,6 +35,7 @@ __export(video_editor_exports, {
|
|
|
35
35
|
TranscriptPanel: () => TranscriptPanel,
|
|
36
36
|
VideoEditor: () => VideoEditor,
|
|
37
37
|
VideoPreview: () => VideoPreview,
|
|
38
|
+
VideoScopes: () => VideoScopes,
|
|
38
39
|
VideoTimeline: () => VideoTimeline,
|
|
39
40
|
VideoTransport: () => VideoTransport,
|
|
40
41
|
useVideoEditor: () => useVideoEditor,
|
|
@@ -84,11 +85,11 @@ function useVideoEditor(controllerOrOptions = {}) {
|
|
|
84
85
|
return { controller, state, ...actions };
|
|
85
86
|
}
|
|
86
87
|
function useVideoEditorTools(registry, controller, options = {}) {
|
|
87
|
-
const { id, namespace, readOnly, rasterize } = options;
|
|
88
|
+
const { id, namespace, readOnly, rasterize, framePixels, framePixelsTimeout } = options;
|
|
88
89
|
(0, import_react.useEffect)(() => {
|
|
89
90
|
if (!registry) return;
|
|
90
|
-
return registry.register((0, import_video_editor.createVideoEditorToolProvider)(controller, { id, namespace, readOnly, rasterize }));
|
|
91
|
-
}, [registry, controller, id, namespace, readOnly, rasterize]);
|
|
91
|
+
return registry.register((0, import_video_editor.createVideoEditorToolProvider)(controller, { id, namespace, readOnly, rasterize, framePixels, framePixelsTimeout }));
|
|
92
|
+
}, [registry, controller, id, namespace, readOnly, rasterize, framePixels, framePixelsTimeout]);
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
// src/video-editor/VideoPreview.tsx
|
|
@@ -119,28 +120,96 @@ function VideoPreview({ controller, resolution, onFrame, className = "", style }
|
|
|
119
120
|
);
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
// src/video-editor/
|
|
123
|
+
// src/video-editor/VideoScopes.tsx
|
|
124
|
+
var import_react3 = require("react");
|
|
123
125
|
var import_video_editor3 = require("@pixerate/editor/video-editor");
|
|
124
126
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
127
|
+
var LABELS = { histogram: "Histogram", waveform: "Luma waveform", vectorscope: "Vectorscope" };
|
|
128
|
+
var pct = (v) => `${Math.round(v * 1e3) / 10}%`;
|
|
129
|
+
function VideoScopes({ controller, scopes = import_video_editor3.VIDEO_SCOPE_KINDS, width = 256, height = 128, className = "" }) {
|
|
130
|
+
const canvases = (0, import_react3.useRef)({});
|
|
131
|
+
const [reading, setReading] = (0, import_react3.useState)(() => ({ ok: false, message: "Show the preview to see its scopes." }));
|
|
132
|
+
const key = scopes.join(",");
|
|
133
|
+
(0, import_react3.useEffect)(() => {
|
|
134
|
+
const feed = controller.previewPixels;
|
|
135
|
+
let frame = 0;
|
|
136
|
+
const raf = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (fn) => setTimeout(fn, 16);
|
|
137
|
+
const caf = typeof cancelAnimationFrame === "function" ? cancelAnimationFrame : (id) => clearTimeout(id);
|
|
138
|
+
const draw = () => {
|
|
139
|
+
frame = 0;
|
|
140
|
+
const next = (0, import_video_editor3.readVideoScopesPanel)(feed, scopes, { width, height });
|
|
141
|
+
setReading(next);
|
|
142
|
+
if (!next.ok) return;
|
|
143
|
+
for (const kind of scopes) {
|
|
144
|
+
const img = next.images[kind];
|
|
145
|
+
const ctx = canvases.current[kind]?.getContext("2d");
|
|
146
|
+
if (img && ctx && typeof ImageData !== "undefined") ctx.putImageData(new ImageData(img.data, img.width, img.height), 0, 0);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
const schedule = () => {
|
|
150
|
+
if (!frame) frame = raf(draw);
|
|
151
|
+
};
|
|
152
|
+
schedule();
|
|
153
|
+
const off = feed.subscribe(schedule);
|
|
154
|
+
return () => {
|
|
155
|
+
off();
|
|
156
|
+
if (frame) caf(frame);
|
|
157
|
+
};
|
|
158
|
+
}, [controller, key, width, height]);
|
|
159
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `flex flex-col gap-2 text-xs ${className}`, "data-video-scopes": "", children: [
|
|
160
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex flex-wrap gap-2", children: scopes.map((kind) => {
|
|
161
|
+
const w = kind === "vectorscope" ? height : width;
|
|
162
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("figure", { className: "m-0 flex min-w-0 max-w-full flex-col gap-1", children: [
|
|
163
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("canvas", { ref: (el) => void (canvases.current[kind] = el), width: w, height, className: "rounded border border-border bg-black", style: { width: w, maxWidth: "100%", height: "auto", aspectRatio: `${w} / ${height}` }, "aria-label": LABELS[kind], "data-scope": kind }),
|
|
164
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("figcaption", { className: "text-[11px] text-muted-foreground", children: LABELS[kind] })
|
|
165
|
+
] }, kind);
|
|
166
|
+
}) }),
|
|
167
|
+
reading.ok ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-col gap-0.5", "data-scope-stats": "", children: [
|
|
168
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "tabular-nums", children: [
|
|
169
|
+
"Luma ",
|
|
170
|
+
reading.stats.luma.mean.toFixed(2),
|
|
171
|
+
" mean \xB7 ",
|
|
172
|
+
reading.stats.luma.p1.toFixed(2),
|
|
173
|
+
"\u2013",
|
|
174
|
+
reading.stats.luma.p99.toFixed(2),
|
|
175
|
+
" \xB7 crushed ",
|
|
176
|
+
pct(reading.stats.clipped.shadows),
|
|
177
|
+
" \xB7 blown ",
|
|
178
|
+
pct(reading.stats.clipped.highlights),
|
|
179
|
+
" \xB7 average",
|
|
180
|
+
" ",
|
|
181
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "inline-block h-2.5 w-2.5 rounded-sm align-middle", style: { background: reading.stats.average } }),
|
|
182
|
+
" ",
|
|
183
|
+
reading.stats.average,
|
|
184
|
+
!reading.complete && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "text-muted-foreground", children: " \xB7 loading" })
|
|
185
|
+
] }),
|
|
186
|
+
reading.notes.map((n) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "text-amber-500", children: n }, n))
|
|
187
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "text-muted-foreground", "data-scope-message": "", children: reading.message })
|
|
188
|
+
] });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/video-editor/VideoTransport.tsx
|
|
192
|
+
var import_video_editor4 = require("@pixerate/editor/video-editor");
|
|
193
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
125
194
|
var btn = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
|
|
126
195
|
function VideoTransport({ controller, editable = true, className = "" }) {
|
|
127
196
|
const state = useVideoEditorState(controller);
|
|
128
197
|
const { time, duration, playing, project, selection, readOnly } = state;
|
|
129
198
|
const canEdit = editable && !readOnly;
|
|
130
|
-
return /* @__PURE__ */ (0,
|
|
131
|
-
/* @__PURE__ */ (0,
|
|
132
|
-
/* @__PURE__ */ (0,
|
|
133
|
-
/* @__PURE__ */ (0,
|
|
134
|
-
/* @__PURE__ */ (0,
|
|
135
|
-
(0,
|
|
199
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `flex flex-wrap items-center gap-1.5 text-xs ${className}`, "data-video-transport": "", children: [
|
|
200
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: btn, onClick: () => controller.step(-1), "aria-label": "Previous frame", title: "Previous frame (\u2190)", "data-step-back": "", children: "\u25C0|" }),
|
|
201
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: btn, onClick: () => controller.toggle(), "aria-label": playing ? "Pause" : "Play", title: "Play / pause (Space)", disabled: duration <= 0, "data-play": "", children: playing ? "Pause" : "Play" }),
|
|
202
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: btn, onClick: () => controller.step(1), "aria-label": "Next frame", title: "Next frame (\u2192)", "data-step-forward": "", children: "|\u25B6" }),
|
|
203
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "px-1 tabular-nums text-muted-foreground", "data-timecode": "", children: [
|
|
204
|
+
(0, import_video_editor4.formatVideoTimecode)(time, project.fps),
|
|
136
205
|
" / ",
|
|
137
|
-
(0,
|
|
206
|
+
(0, import_video_editor4.formatVideoTimecode)(duration, project.fps)
|
|
138
207
|
] }),
|
|
139
|
-
editable && /* @__PURE__ */ (0,
|
|
140
|
-
/* @__PURE__ */ (0,
|
|
141
|
-
/* @__PURE__ */ (0,
|
|
142
|
-
/* @__PURE__ */ (0,
|
|
143
|
-
/* @__PURE__ */ (0,
|
|
208
|
+
editable && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
209
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-1 h-4 w-px bg-border" }),
|
|
210
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: btn, onClick: () => controller.splitAtPlayhead(), disabled: !canEdit, title: "Split at the playhead (S)", "data-split": "", children: "Split" }),
|
|
211
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: btn, onClick: () => controller.deleteSelection(), disabled: !canEdit || !selection.length, title: "Delete the selected clips (Delete)", "data-delete": "", children: "Delete" }),
|
|
212
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
144
213
|
"button",
|
|
145
214
|
{
|
|
146
215
|
type: "button",
|
|
@@ -152,16 +221,16 @@ function VideoTransport({ controller, editable = true, className = "" }) {
|
|
|
152
221
|
children: "Ripple delete"
|
|
153
222
|
}
|
|
154
223
|
),
|
|
155
|
-
/* @__PURE__ */ (0,
|
|
156
|
-
/* @__PURE__ */ (0,
|
|
224
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: btn, onClick: () => controller.undo(), disabled: !canEdit || !state.canUndo, title: state.canUndo ? `Undo ${state.undoLabel}` : "Nothing to undo", "data-undo": "", children: "Undo" }),
|
|
225
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: btn, onClick: () => controller.redo(), disabled: !canEdit || !state.canRedo, title: state.canRedo ? `Redo ${state.redoLabel}` : "Nothing to redo", "data-redo": "", children: "Redo" })
|
|
157
226
|
] })
|
|
158
227
|
] });
|
|
159
228
|
}
|
|
160
229
|
|
|
161
230
|
// src/video-editor/VideoTimeline.tsx
|
|
162
|
-
var
|
|
163
|
-
var
|
|
164
|
-
var
|
|
231
|
+
var import_react4 = require("react");
|
|
232
|
+
var import_video_editor5 = require("@pixerate/editor/video-editor");
|
|
233
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
165
234
|
var HEADER = 150;
|
|
166
235
|
var RULER = 24;
|
|
167
236
|
var ROW = 44;
|
|
@@ -170,48 +239,48 @@ var DRAG_THRESHOLD = 3;
|
|
|
170
239
|
var MIN_ZOOM = 10;
|
|
171
240
|
var MAX_ZOOM = 400;
|
|
172
241
|
function ClipWaveform({ controller, clip, asset, width, height }) {
|
|
173
|
-
const peaks = (0,
|
|
242
|
+
const peaks = (0, import_react4.useSyncExternalStore)(
|
|
174
243
|
(notify) => controller.audioPeaks.subscribe(notify),
|
|
175
244
|
() => controller.audioPeaks.peek(asset),
|
|
176
245
|
() => void 0
|
|
177
246
|
);
|
|
178
|
-
(0,
|
|
247
|
+
(0, import_react4.useEffect)(() => {
|
|
179
248
|
controller.audioPeaks.load(asset).catch(() => {
|
|
180
249
|
});
|
|
181
250
|
}, [controller, asset]);
|
|
182
|
-
const d = (0,
|
|
251
|
+
const d = (0, import_react4.useMemo)(() => peaks ? (0, import_video_editor5.videoClipWaveformPath)(clip, peaks, width, height) : "", [peaks, clip, width, height]);
|
|
183
252
|
if (!d) return null;
|
|
184
|
-
return /* @__PURE__ */ (0,
|
|
253
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("svg", { className: "pointer-events-none absolute inset-0", width, height, "aria-hidden": "true", "data-waveform": clip.id, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d, fill: "currentColor", fillOpacity: 0.45 }) });
|
|
185
254
|
}
|
|
186
255
|
var isField = (el) => el instanceof HTMLElement && /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName);
|
|
187
256
|
function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable = true, waveforms = "audio", className = "" }) {
|
|
188
257
|
const state = useVideoEditorState(controller);
|
|
189
258
|
const { project, duration, time, selection, readOnly, error } = state;
|
|
190
|
-
const [ownZoom, setOwnZoom] = (0,
|
|
259
|
+
const [ownZoom, setOwnZoom] = (0, import_react4.useState)(pxPerSecond ?? 100);
|
|
191
260
|
const px = onPxPerSecondChange ? pxPerSecond ?? 100 : ownZoom;
|
|
192
261
|
const canEdit = editable && !readOnly;
|
|
193
|
-
const [drag, setDrag] = (0,
|
|
194
|
-
const dragRef = (0,
|
|
195
|
-
const rows = (0,
|
|
196
|
-
const ruler = (0,
|
|
197
|
-
const assets = (0,
|
|
262
|
+
const [drag, setDrag] = (0, import_react4.useState)(null);
|
|
263
|
+
const dragRef = (0, import_react4.useRef)(null);
|
|
264
|
+
const rows = (0, import_react4.useRef)(/* @__PURE__ */ new Map());
|
|
265
|
+
const ruler = (0, import_react4.useRef)(null);
|
|
266
|
+
const assets = (0, import_video_editor5.videoAssetMap)(project);
|
|
198
267
|
const msToPx = (ms) => ms * px / 1e3;
|
|
199
268
|
const span = Math.max(duration + 5e3, 1e4);
|
|
200
269
|
const width = msToPx(span);
|
|
201
|
-
const ticks = (0,
|
|
270
|
+
const ticks = (0, import_video_editor5.videoRulerTicks)(span, px, project.fps);
|
|
202
271
|
const seekAt = (clientX) => {
|
|
203
272
|
const box = ruler.current.getBoundingClientRect();
|
|
204
273
|
controller.seek(Math.max(0, (clientX - box.left) * 1e3 / px));
|
|
205
274
|
};
|
|
206
275
|
const trackAt = (clipId, clientY) => {
|
|
207
|
-
const allowed = (0,
|
|
276
|
+
const allowed = (0, import_video_editor5.videoDropTracks)(project, clipId);
|
|
208
277
|
for (const [id, el] of rows.current) {
|
|
209
278
|
const box = el.getBoundingClientRect();
|
|
210
279
|
if (clientY >= box.top && clientY < box.bottom && allowed.includes(id)) return id;
|
|
211
280
|
}
|
|
212
281
|
return void 0;
|
|
213
282
|
};
|
|
214
|
-
const planFor = (d, e) => (0,
|
|
283
|
+
const planFor = (d, e) => (0, import_video_editor5.planVideoClipDrag)(project, {
|
|
215
284
|
clip: d.clip,
|
|
216
285
|
mode: d.mode,
|
|
217
286
|
delta: (e.clientX - d.x) * 1e3 / px,
|
|
@@ -224,10 +293,10 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
224
293
|
dragRef.current = next;
|
|
225
294
|
setDrag(next);
|
|
226
295
|
};
|
|
227
|
-
const live = (0,
|
|
296
|
+
const live = (0, import_react4.useRef)({ planFor, selection });
|
|
228
297
|
live.current = { planFor, selection };
|
|
229
|
-
const stopDrag = (0,
|
|
230
|
-
(0,
|
|
298
|
+
const stopDrag = (0, import_react4.useRef)(null);
|
|
299
|
+
(0, import_react4.useEffect)(() => () => stopDrag.current?.(), []);
|
|
231
300
|
const onClipPointerDown = (e, clip, track) => {
|
|
232
301
|
if (e.button !== void 0 && e.button !== 0) return;
|
|
233
302
|
e.stopPropagation();
|
|
@@ -259,7 +328,7 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
259
328
|
return;
|
|
260
329
|
}
|
|
261
330
|
const plan = live.current.planFor(d, ev);
|
|
262
|
-
if (plan?.edit) controller.apply(plan.edit, (0,
|
|
331
|
+
if (plan?.edit) controller.apply(plan.edit, (0, import_video_editor5.videoDragLabel)(plan.edit));
|
|
263
332
|
};
|
|
264
333
|
const cancel = () => {
|
|
265
334
|
stop();
|
|
@@ -305,7 +374,7 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
305
374
|
}).filter((x) => x !== null)
|
|
306
375
|
)
|
|
307
376
|
}));
|
|
308
|
-
const toggle = (track, field2, label) => /* @__PURE__ */ (0,
|
|
377
|
+
const toggle = (track, field2, label) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
309
378
|
"button",
|
|
310
379
|
{
|
|
311
380
|
type: "button",
|
|
@@ -319,12 +388,12 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
319
388
|
children: field2 === "muted" ? "M" : field2 === "hidden" ? "H" : "L"
|
|
320
389
|
}
|
|
321
390
|
);
|
|
322
|
-
return /* @__PURE__ */ (0,
|
|
323
|
-
!onPxPerSecondChange && /* @__PURE__ */ (0,
|
|
391
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: `flex flex-col gap-1 ${className}`, "data-video-timeline-wrapper": "", children: [
|
|
392
|
+
!onPxPerSecondChange && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center gap-2 self-end text-xs text-muted-foreground", children: [
|
|
324
393
|
"Zoom",
|
|
325
|
-
/* @__PURE__ */ (0,
|
|
394
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("input", { type: "range", min: MIN_ZOOM, max: MAX_ZOOM, step: 5, value: ownZoom, "aria-label": "Timeline zoom", onChange: (e) => setOwnZoom(Number(e.target.value)), "data-zoom": "" })
|
|
326
395
|
] }),
|
|
327
|
-
/* @__PURE__ */ (0,
|
|
396
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
328
397
|
"div",
|
|
329
398
|
{
|
|
330
399
|
className: "relative overflow-x-auto rounded border border-border outline-none focus-visible:ring-2 focus-visible:ring-rose-500/50",
|
|
@@ -333,10 +402,10 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
333
402
|
"aria-label": "Video timeline",
|
|
334
403
|
onKeyDown,
|
|
335
404
|
"data-video-timeline": "",
|
|
336
|
-
children: /* @__PURE__ */ (0,
|
|
337
|
-
/* @__PURE__ */ (0,
|
|
338
|
-
/* @__PURE__ */ (0,
|
|
339
|
-
/* @__PURE__ */ (0,
|
|
405
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "relative", style: { width: HEADER + width }, children: [
|
|
406
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex", style: { height: RULER }, children: [
|
|
407
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "sticky left-0 z-20 shrink-0 border-b border-r border-border bg-background", style: { width: HEADER } }),
|
|
408
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
340
409
|
"div",
|
|
341
410
|
{
|
|
342
411
|
ref: ruler,
|
|
@@ -359,13 +428,13 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
359
428
|
if (e.buttons & 1) seekAt(e.clientX);
|
|
360
429
|
},
|
|
361
430
|
children: [
|
|
362
|
-
ticks.map((t) => /* @__PURE__ */ (0,
|
|
363
|
-
(project.markers ?? []).map((m) => /* @__PURE__ */ (0,
|
|
431
|
+
ticks.map((t) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: `absolute bottom-0 w-px ${t.major ? "h-2.5 bg-muted-foreground/70" : "h-1 bg-muted-foreground/40"}`, style: { left: t.x }, "data-tick": t.time, children: t.label && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "absolute -top-3.5 left-1 whitespace-nowrap tabular-nums", children: t.label }) }, t.time)),
|
|
432
|
+
(project.markers ?? []).map((m) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
364
433
|
"div",
|
|
365
434
|
{
|
|
366
435
|
className: "absolute top-0 h-2.5 w-2.5 -translate-x-1/2 rotate-45",
|
|
367
436
|
style: { left: msToPx(m.time), background: m.color ?? "#f59e0b" },
|
|
368
|
-
title: `${m.label ?? m.id} at ${(0,
|
|
437
|
+
title: `${m.label ?? m.id} at ${(0, import_video_editor5.formatVideoSeconds)(m.time)}`,
|
|
369
438
|
"data-marker": m.id
|
|
370
439
|
},
|
|
371
440
|
m.id
|
|
@@ -374,19 +443,19 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
374
443
|
}
|
|
375
444
|
)
|
|
376
445
|
] }),
|
|
377
|
-
placed.map(({ track, clips }) => /* @__PURE__ */ (0,
|
|
378
|
-
/* @__PURE__ */ (0,
|
|
379
|
-
/* @__PURE__ */ (0,
|
|
380
|
-
/* @__PURE__ */ (0,
|
|
381
|
-
/* @__PURE__ */ (0,
|
|
446
|
+
placed.map(({ track, clips }) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex border-b border-border last:border-b-0", style: { height: ROW }, "data-track": track.id, children: [
|
|
447
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "sticky left-0 z-20 flex shrink-0 flex-col justify-center gap-0.5 border-r border-border bg-background px-2 text-xs", style: { width: HEADER }, "data-track-header": "", children: [
|
|
448
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center gap-1", children: [
|
|
449
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "truncate font-medium", children: track.name ?? track.id }),
|
|
450
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "rounded bg-muted px-1 text-[10px] uppercase text-muted-foreground", "data-track-kind": "", children: track.kind })
|
|
382
451
|
] }),
|
|
383
|
-
editable && /* @__PURE__ */ (0,
|
|
452
|
+
editable && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex gap-1", children: [
|
|
384
453
|
toggle(track, "muted", "Mute"),
|
|
385
454
|
track.kind === "video" && toggle(track, "hidden", "Hide"),
|
|
386
455
|
toggle(track, "locked", "Lock")
|
|
387
456
|
] })
|
|
388
457
|
] }),
|
|
389
|
-
/* @__PURE__ */ (0,
|
|
458
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
390
459
|
"div",
|
|
391
460
|
{
|
|
392
461
|
ref: (el) => {
|
|
@@ -402,35 +471,35 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
402
471
|
children: clips.map(({ clip, home, start, duration: length, dragging }) => {
|
|
403
472
|
const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
|
|
404
473
|
const selected = selection.includes(clip.id);
|
|
405
|
-
const label = (0,
|
|
406
|
-
return /* @__PURE__ */ (0,
|
|
474
|
+
const label = (0, import_video_editor5.videoClipLabel)(clip, asset);
|
|
475
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
407
476
|
"div",
|
|
408
477
|
{
|
|
409
478
|
className: `absolute top-1 bottom-1 select-none touch-none overflow-hidden rounded text-[11px] text-white ${canEdit && !home.locked ? "cursor-grab" : "cursor-default"} ${dragging ? "opacity-80 shadow-lg" : ""}`,
|
|
410
479
|
style: {
|
|
411
480
|
left: msToPx(start),
|
|
412
481
|
width: Math.max(2, msToPx(length)),
|
|
413
|
-
background: (0,
|
|
482
|
+
background: (0, import_video_editor5.videoClipTint)(clip, asset),
|
|
414
483
|
outline: selected ? "2px solid #f43f5e" : void 0,
|
|
415
484
|
outlineOffset: selected ? 1 : void 0,
|
|
416
485
|
zIndex: dragging ? 10 : selected ? 2 : 1
|
|
417
486
|
},
|
|
418
|
-
title: `${label} \xB7 ${(0,
|
|
487
|
+
title: `${label} \xB7 ${(0, import_video_editor5.formatVideoSeconds)(start)}\u2013${(0, import_video_editor5.formatVideoSeconds)(start + length)}`,
|
|
419
488
|
"data-clip": clip.id,
|
|
420
489
|
"data-selected": selected ? "" : void 0,
|
|
421
490
|
"data-dragging": dragging ? "" : void 0,
|
|
422
491
|
"aria-selected": selected,
|
|
423
492
|
onPointerDown: (e) => onClipPointerDown(e, clip, home),
|
|
424
493
|
children: [
|
|
425
|
-
(0,
|
|
426
|
-
/* @__PURE__ */ (0,
|
|
427
|
-
clip.transition && /* @__PURE__ */ (0,
|
|
494
|
+
(0, import_video_editor5.videoClipShowsWaveform)(clip, asset, waveforms) && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ClipWaveform, { controller, clip, asset, width: Math.max(2, msToPx(clip.duration)), height: ROW - 8 }),
|
|
495
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "pointer-events-none relative block truncate px-1.5 py-0.5 leading-4", children: label }),
|
|
496
|
+
clip.transition && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "pointer-events-none absolute left-1 bottom-0.5 rounded bg-black/40 px-1 text-[9px] leading-3", title: `${clip.transition.type} ${clip.transition.duration} ms`, "data-transition": clip.transition.type, children: [
|
|
428
497
|
"\u29D3 ",
|
|
429
498
|
clip.transition.type
|
|
430
499
|
] }),
|
|
431
|
-
canEdit && !home.locked && /* @__PURE__ */ (0,
|
|
432
|
-
/* @__PURE__ */ (0,
|
|
433
|
-
/* @__PURE__ */ (0,
|
|
500
|
+
canEdit && !home.locked && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
501
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "absolute inset-y-0 left-0 cursor-ew-resize bg-white/0 hover:bg-white/40", style: { width: HANDLE }, "data-handle": "start", "aria-hidden": "true" }),
|
|
502
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "absolute inset-y-0 right-0 cursor-ew-resize bg-white/0 hover:bg-white/40", style: { width: HANDLE }, "data-handle": "end", "aria-hidden": "true" })
|
|
434
503
|
] })
|
|
435
504
|
]
|
|
436
505
|
},
|
|
@@ -440,17 +509,18 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
440
509
|
}
|
|
441
510
|
)
|
|
442
511
|
] }, track.id)),
|
|
443
|
-
/* @__PURE__ */ (0,
|
|
512
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "pointer-events-none absolute top-0 bottom-0 w-0.5 -translate-x-1/2 bg-rose-500", style: { left: HEADER + msToPx(time), zIndex: 15 }, "data-playhead": "" })
|
|
444
513
|
] })
|
|
445
514
|
}
|
|
446
515
|
),
|
|
447
|
-
error && /* @__PURE__ */ (0,
|
|
516
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-xs text-red-500", role: "alert", "data-edit-error": "", children: error })
|
|
448
517
|
] });
|
|
449
518
|
}
|
|
450
519
|
|
|
451
520
|
// src/video-editor/ClipInspector.tsx
|
|
452
|
-
var
|
|
453
|
-
var
|
|
521
|
+
var import_react5 = require("react");
|
|
522
|
+
var import_video_editor6 = require("@pixerate/editor/video-editor");
|
|
523
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
454
524
|
var btn2 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
|
|
455
525
|
var field = "w-full rounded border border-border bg-transparent px-1 py-0.5 text-xs disabled:opacity-50";
|
|
456
526
|
var DEFAULT_TRANSITION = 500;
|
|
@@ -464,7 +534,7 @@ function Commit({
|
|
|
464
534
|
if (raw !== value) onCommit(raw);
|
|
465
535
|
};
|
|
466
536
|
if (multiline)
|
|
467
|
-
return /* @__PURE__ */ (0,
|
|
537
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
468
538
|
"textarea",
|
|
469
539
|
{
|
|
470
540
|
className: field,
|
|
@@ -476,11 +546,64 @@ function Commit({
|
|
|
476
546
|
},
|
|
477
547
|
value
|
|
478
548
|
);
|
|
479
|
-
return /* @__PURE__ */ (0,
|
|
549
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("input", { className: field, ...rest, defaultValue: value, onKeyDown: (e) => e.key === "Enter" && e.currentTarget.blur(), onBlur: (e) => commit(e.currentTarget.value) }, value);
|
|
550
|
+
}
|
|
551
|
+
function LutControls({ controller, clip, disabled }) {
|
|
552
|
+
const [message, setMessage] = (0, import_react5.useState)(null);
|
|
553
|
+
const lut = clip.grade?.lut;
|
|
554
|
+
const apply = (setting, label) => {
|
|
555
|
+
const r = controller.apply((0, import_video_editor6.videoLutEdits)(controller.getProject(), [clip.id], setting), label);
|
|
556
|
+
setMessage(r.ok ? null : r.error);
|
|
557
|
+
};
|
|
558
|
+
const choose = async (src) => {
|
|
559
|
+
if (!src) return lut && apply(null, "Remove LUT");
|
|
560
|
+
try {
|
|
561
|
+
const table = await controller.luts.load(src);
|
|
562
|
+
apply({ src, ...lut?.strength !== void 0 ? { strength: lut.strength } : {}, ...table.title ? { name: table.title } : {} }, "Apply LUT");
|
|
563
|
+
} catch (e) {
|
|
564
|
+
setMessage(e instanceof Error ? e.message : String(e));
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
const readFile = async (file) => {
|
|
568
|
+
if (!file) return;
|
|
569
|
+
try {
|
|
570
|
+
const table = controller.luts.set(file.name, await file.text());
|
|
571
|
+
apply({ src: file.name, ...lut?.strength !== void 0 ? { strength: lut.strength } : {}, ...table.title ? { name: table.title } : {} }, "Apply LUT");
|
|
572
|
+
} catch (e) {
|
|
573
|
+
setMessage(`${file.name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
|
|
577
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "LUT", name: "grade.lut", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex min-w-0 gap-1", children: [
|
|
578
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Commit, { value: lut?.src ?? "", placeholder: "look.cube", "aria-label": "LUT file", disabled, onCommit: (raw) => void choose(raw.trim()) }),
|
|
579
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: `${btn2} cursor-pointer whitespace-nowrap`, title: "Load a .cube file", children: [
|
|
580
|
+
"Load",
|
|
581
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("input", { type: "file", accept: ".cube", className: "sr-only", disabled, "aria-label": "Load a .cube LUT", onChange: (e) => void readFile(e.currentTarget.files?.[0]) })
|
|
582
|
+
] }),
|
|
583
|
+
lut && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: btn2, disabled, "aria-label": "Remove LUT", onClick: () => apply(null, "Remove LUT"), children: "\u2715" })
|
|
584
|
+
] }) }),
|
|
585
|
+
lut && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "LUT strength", name: "grade.lut.strength", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
586
|
+
Commit,
|
|
587
|
+
{
|
|
588
|
+
type: "number",
|
|
589
|
+
min: 0,
|
|
590
|
+
max: 1,
|
|
591
|
+
step: 0.05,
|
|
592
|
+
value: String(lut.strength ?? 1),
|
|
593
|
+
"aria-label": "LUT strength",
|
|
594
|
+
disabled,
|
|
595
|
+
onCommit: (raw) => {
|
|
596
|
+
const n = Number(raw);
|
|
597
|
+
if (raw.trim() && Number.isFinite(n)) apply(n > 0 ? { ...lut, strength: Math.min(1, n) } : null, n > 0 ? "Change LUT strength" : "Remove LUT");
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
) }),
|
|
601
|
+
message && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-xs text-red-500", role: "alert", "data-lut-error": "", children: message })
|
|
602
|
+
] });
|
|
480
603
|
}
|
|
481
604
|
function Row({ label, name, children }) {
|
|
482
|
-
return /* @__PURE__ */ (0,
|
|
483
|
-
/* @__PURE__ */ (0,
|
|
605
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "grid grid-cols-[5.5rem_minmax(0,1fr)] items-center gap-2 text-xs", "data-field": name, children: [
|
|
606
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-muted-foreground", children: label }),
|
|
484
607
|
children
|
|
485
608
|
] });
|
|
486
609
|
}
|
|
@@ -488,8 +611,8 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
488
611
|
const state = useVideoEditorState(controller);
|
|
489
612
|
const { project, selection, readOnly, time } = state;
|
|
490
613
|
if (selection.length !== 1)
|
|
491
|
-
return /* @__PURE__ */ (0,
|
|
492
|
-
const at = (0,
|
|
614
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: `text-xs text-muted-foreground ${className}`, "data-clip-inspector": "", children: selection.length ? `${selection.length} clips selected. Select one clip to edit it.` : "Select a clip on the timeline to edit it." });
|
|
615
|
+
const at = (0, import_video_editor6.findVideoClip)(project, selection[0]);
|
|
493
616
|
if (!at) return null;
|
|
494
617
|
const clip = at.clip;
|
|
495
618
|
const disabled = readOnly || !!at.track.locked;
|
|
@@ -505,24 +628,24 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
505
628
|
};
|
|
506
629
|
const text = (key, label) => {
|
|
507
630
|
const value = clip[key];
|
|
508
|
-
return /* @__PURE__ */ (0,
|
|
631
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Commit, { value: value === void 0 ? "" : String(value), "aria-label": label ?? key, disabled, onCommit: (raw) => set(key, raw.trim() || void 0, `Change ${label ?? key}`) });
|
|
509
632
|
};
|
|
510
633
|
const number = (key, label, attrs = {}) => {
|
|
511
634
|
const value = clip[key];
|
|
512
|
-
return /* @__PURE__ */ (0,
|
|
635
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Commit, { type: "number", value: value === void 0 ? "" : String(value), "aria-label": label, disabled, onCommit: (raw) => num(key, raw, `Change ${label.toLowerCase()}`), ...attrs });
|
|
513
636
|
};
|
|
514
637
|
const transition = clip.transition;
|
|
515
|
-
return /* @__PURE__ */ (0,
|
|
516
|
-
/* @__PURE__ */ (0,
|
|
517
|
-
/* @__PURE__ */ (0,
|
|
518
|
-
/* @__PURE__ */ (0,
|
|
519
|
-
at.track.locked && /* @__PURE__ */ (0,
|
|
638
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: `flex flex-col gap-1.5 ${className}`, "data-clip-inspector": "", "data-clip-id": clip.id, children: [
|
|
639
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-2 text-xs", children: [
|
|
640
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "font-medium", children: clip.type === "media" ? "Media clip" : clip.type === "text" ? "Title" : "Colour clip" }),
|
|
641
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("code", { className: "text-muted-foreground", children: clip.id }),
|
|
642
|
+
at.track.locked && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-amber-500", children: "locked track" })
|
|
520
643
|
] }),
|
|
521
|
-
/* @__PURE__ */ (0,
|
|
522
|
-
/* @__PURE__ */ (0,
|
|
523
|
-
/* @__PURE__ */ (0,
|
|
524
|
-
(0,
|
|
525
|
-
/* @__PURE__ */ (0,
|
|
644
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Name", name: "name", children: text("name", "Name") }),
|
|
645
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Start", name: "start", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "tabular-nums text-xs", children: (0, import_video_editor6.formatVideoSeconds)(clip.start) }) }),
|
|
646
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Duration", name: "duration", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "tabular-nums text-xs", children: (0, import_video_editor6.formatVideoSeconds)(clip.duration) }) }),
|
|
647
|
+
(0, import_video_editor6.videoKeyframeRows)(project, clip.id, time).map((row) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: row.label, name: row.property, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "flex min-w-0 items-center gap-1", "data-keyframe-row": row.property, children: [
|
|
648
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
526
649
|
Commit,
|
|
527
650
|
{
|
|
528
651
|
type: "number",
|
|
@@ -536,12 +659,12 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
536
659
|
onCommit: (raw) => {
|
|
537
660
|
const n = Number(raw);
|
|
538
661
|
if (raw.trim() && !Number.isFinite(n)) return;
|
|
539
|
-
const edit = (0,
|
|
662
|
+
const edit = (0, import_video_editor6.videoKeyframeValueEdit)(project, clip.id, row.property, time, raw.trim() ? n : void 0);
|
|
540
663
|
if (edit) controller.apply(edit, row.keyed ? `Keyframe ${row.label.toLowerCase()}` : `Change ${row.label.toLowerCase()}`);
|
|
541
664
|
}
|
|
542
665
|
}
|
|
543
666
|
),
|
|
544
|
-
/* @__PURE__ */ (0,
|
|
667
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
545
668
|
"button",
|
|
546
669
|
{
|
|
547
670
|
type: "button",
|
|
@@ -552,17 +675,17 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
552
675
|
disabled,
|
|
553
676
|
"data-keyframe-toggle": row.property,
|
|
554
677
|
onClick: () => {
|
|
555
|
-
const edit = (0,
|
|
678
|
+
const edit = (0, import_video_editor6.videoKeyframeToggleEdit)(project, clip.id, row.property, time);
|
|
556
679
|
if (edit) controller.apply(edit, `${row.onKey ? "Remove" : "Add"} ${row.label.toLowerCase()} keyframe`);
|
|
557
680
|
},
|
|
558
681
|
children: row.onKey ? "\u25C6" : "\u25C7"
|
|
559
682
|
}
|
|
560
683
|
),
|
|
561
|
-
row.keyed && /* @__PURE__ */ (0,
|
|
684
|
+
row.keyed && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "shrink-0 text-[10px] tabular-nums text-muted-foreground", "data-keyframe-count": row.property, children: row.keys })
|
|
562
685
|
] }) }, row.property)),
|
|
563
|
-
at.track.kind === "video" && /* @__PURE__ */ (0,
|
|
564
|
-
at.track.kind === "video" && /* @__PURE__ */ (0,
|
|
565
|
-
/* @__PURE__ */ (0,
|
|
686
|
+
at.track.kind === "video" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Blend", name: "blend", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("select", { className: field, value: clip.blend ?? "normal", disabled, "aria-label": "Blend mode", onChange: (e) => set("blend", e.target.value === "normal" ? void 0 : e.target.value, "Change blend mode"), children: import_video_editor6.VIDEO_BLEND_MODES.map((m) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: m, children: import_video_editor6.VIDEO_BLEND_MODE_LABELS[m] }, m)) }) }),
|
|
687
|
+
at.track.kind === "video" && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
|
|
688
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Look", name: "gradePreset", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
566
689
|
"select",
|
|
567
690
|
{
|
|
568
691
|
className: field,
|
|
@@ -570,16 +693,16 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
570
693
|
disabled,
|
|
571
694
|
"aria-label": "Apply a look",
|
|
572
695
|
onChange: (e) => {
|
|
573
|
-
const grade = (0,
|
|
696
|
+
const grade = (0, import_video_editor6.videoGradeLook)(clip.grade, e.target.value);
|
|
574
697
|
if (grade) controller.apply({ op: "update_clip", clip: clip.id, set: { grade } }, `Apply ${e.target.value} look`);
|
|
575
698
|
},
|
|
576
699
|
children: [
|
|
577
|
-
/* @__PURE__ */ (0,
|
|
578
|
-
|
|
700
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: clip.grade ? "Graded" : "None" }),
|
|
701
|
+
import_video_editor6.VIDEO_GRADE_PRESET_NAMES.map((name) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: name, children: name.replace(/-/g, " ") }, name))
|
|
579
702
|
]
|
|
580
703
|
}
|
|
581
704
|
) }),
|
|
582
|
-
|
|
705
|
+
import_video_editor6.VIDEO_GRADE_CONTROLS.map((c) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: c.label, name: `grade.${c.key}`, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
583
706
|
Commit,
|
|
584
707
|
{
|
|
585
708
|
type: "number",
|
|
@@ -593,13 +716,14 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
593
716
|
onCommit: (raw) => {
|
|
594
717
|
const n = raw.trim() ? Number(raw) : 0;
|
|
595
718
|
if (!Number.isFinite(n)) return;
|
|
596
|
-
const grade = (0,
|
|
719
|
+
const grade = (0, import_video_editor6.normalizeVideoGrade)({ ...clip.grade, [c.key]: n });
|
|
597
720
|
set("grade", grade, `Change ${c.label.toLowerCase()}`);
|
|
598
721
|
}
|
|
599
722
|
}
|
|
600
|
-
) }, c.key))
|
|
723
|
+
) }, c.key)),
|
|
724
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(LutControls, { controller, clip, disabled })
|
|
601
725
|
] }),
|
|
602
|
-
at.track.kind === "video" && ["animateIn", "animateOut"].map((key) => /* @__PURE__ */ (0,
|
|
726
|
+
at.track.kind === "video" && ["animateIn", "animateOut"].map((key) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: key === "animateIn" ? "Animate in" : "Animate out", name: key, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
603
727
|
"select",
|
|
604
728
|
{
|
|
605
729
|
className: field,
|
|
@@ -611,35 +735,35 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
611
735
|
set(key, effect ? { ...clip[key], effect } : void 0, key === "animateIn" ? "Change entrance" : "Change exit");
|
|
612
736
|
},
|
|
613
737
|
children: [
|
|
614
|
-
/* @__PURE__ */ (0,
|
|
615
|
-
|
|
738
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: "None" }),
|
|
739
|
+
import_video_editor6.VIDEO_CLIP_ANIMATION_EFFECTS.map((effect) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: effect, children: import_video_editor6.VIDEO_CLIP_ANIMATION_LABELS[effect] }, effect))
|
|
616
740
|
]
|
|
617
741
|
}
|
|
618
742
|
) }, key)),
|
|
619
|
-
/* @__PURE__ */ (0,
|
|
620
|
-
/* @__PURE__ */ (0,
|
|
621
|
-
clip.type === "media" && /* @__PURE__ */ (0,
|
|
622
|
-
/* @__PURE__ */ (0,
|
|
623
|
-
/* @__PURE__ */ (0,
|
|
624
|
-
/* @__PURE__ */ (0,
|
|
743
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Fade in (ms)", name: "fadeIn", children: number("fadeIn", "Fade in", { min: 0, step: 100 }) }),
|
|
744
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Fade out (ms)", name: "fadeOut", children: number("fadeOut", "Fade out", { min: 0, step: 100 }) }),
|
|
745
|
+
clip.type === "media" && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
|
|
746
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Speed", name: "speed", children: clip.timeRemap ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "flex items-center gap-1", "data-ramped": "", children: [
|
|
747
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-muted-foreground", children: "ramped" }),
|
|
748
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: btn2, disabled, onClick: () => controller.apply({ op: "set_time_remap", clip: clip.id, keys: null, ripple: true }, "Remove speed ramp"), children: "Remove ramp" })
|
|
625
749
|
] }) : number("speed", "Speed", { min: 0.1, step: 0.25 }) }),
|
|
626
|
-
project.assets.find((a) => a.id === clip.asset)?.kind !== "image" && /* @__PURE__ */ (0,
|
|
627
|
-
/* @__PURE__ */ (0,
|
|
750
|
+
project.assets.find((a) => a.id === clip.asset)?.kind !== "image" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Retime", name: "retime", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "flex flex-wrap gap-1", children: [
|
|
751
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
628
752
|
"button",
|
|
629
753
|
{
|
|
630
754
|
type: "button",
|
|
631
755
|
className: btn2,
|
|
632
756
|
disabled: disabled || time < clip.start || time > clip.start + clip.duration,
|
|
633
757
|
title: "Hold the frame at the playhead for a second, pushing later clips along",
|
|
634
|
-
onClick: () => controller.apply((0,
|
|
758
|
+
onClick: () => controller.apply((0, import_video_editor6.videoFreezeFrameEdit)(clip, time - clip.start, 1e3), "Freeze frame"),
|
|
635
759
|
children: "Freeze 1 s here"
|
|
636
760
|
}
|
|
637
761
|
),
|
|
638
|
-
/* @__PURE__ */ (0,
|
|
762
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: btn2, disabled, title: "Normal speed, then a glide into 0.3\xD7 slow motion", onClick: () => controller.apply((0, import_video_editor6.videoSpeedRampEdit)(clip, (0, import_video_editor6.videoSlowMotionRamp)(clip.duration)), "Speed ramp"), children: "Slow-mo ramp" })
|
|
639
763
|
] }) }),
|
|
640
|
-
/* @__PURE__ */ (0,
|
|
764
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Muted", name: "muted", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("input", { type: "checkbox", className: "justify-self-start", checked: !!clip.muted, disabled, "aria-label": "Muted", onChange: (e) => set("muted", e.target.checked || void 0, e.target.checked ? "Mute clip" : "Unmute clip") }) }),
|
|
641
765
|
["fadeInCurve", "fadeOutCurve"].map(
|
|
642
|
-
(key) => (key === "fadeInCurve" ? clip.fadeIn : clip.fadeOut) ? /* @__PURE__ */ (0,
|
|
766
|
+
(key) => (key === "fadeInCurve" ? clip.fadeIn : clip.fadeOut) ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: key === "fadeInCurve" ? "Fade in sound" : "Fade out sound", name: key, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
643
767
|
"select",
|
|
644
768
|
{
|
|
645
769
|
className: field,
|
|
@@ -650,19 +774,19 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
650
774
|
const curve = e.target.value;
|
|
651
775
|
set(key, curve === "linear" ? void 0 : curve, key === "fadeInCurve" ? "Change fade in curve" : "Change fade out curve");
|
|
652
776
|
},
|
|
653
|
-
children:
|
|
777
|
+
children: import_video_editor6.VIDEO_FADE_CURVES.map((c) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: c, children: import_video_editor6.VIDEO_FADE_CURVE_LABELS[c] }, c))
|
|
654
778
|
}
|
|
655
779
|
) }, key) : null
|
|
656
780
|
)
|
|
657
781
|
] }),
|
|
658
|
-
clip.type === "text" && /* @__PURE__ */ (0,
|
|
659
|
-
/* @__PURE__ */ (0,
|
|
660
|
-
/* @__PURE__ */ (0,
|
|
661
|
-
/* @__PURE__ */ (0,
|
|
662
|
-
/* @__PURE__ */ (0,
|
|
782
|
+
clip.type === "text" && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
|
|
783
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Text", name: "text", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Commit, { multiline: true, value: clip.text, "aria-label": "Text", disabled, onCommit: (raw) => set("text", raw, "Change text") }) }),
|
|
784
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Colour", name: "color", children: text("color", "Colour") }),
|
|
785
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Font size", name: "fontSize", children: number("fontSize", "Font size", { min: 1, step: 2 }) }),
|
|
786
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Background", name: "background", children: text("background", "Background") })
|
|
663
787
|
] }),
|
|
664
|
-
clip.type === "color" && /* @__PURE__ */ (0,
|
|
665
|
-
/* @__PURE__ */ (0,
|
|
788
|
+
clip.type === "color" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Colour", name: "color", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Commit, { value: clip.color, "aria-label": "Colour", disabled, onCommit: (raw) => raw.trim() && set("color", raw.trim(), "Change colour") }) }),
|
|
789
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Transition in", name: "transition", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
666
790
|
"select",
|
|
667
791
|
{
|
|
668
792
|
className: field,
|
|
@@ -677,12 +801,12 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
677
801
|
);
|
|
678
802
|
},
|
|
679
803
|
children: [
|
|
680
|
-
/* @__PURE__ */ (0,
|
|
681
|
-
|
|
804
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: "None" }),
|
|
805
|
+
import_video_editor6.VIDEO_TRANSITION_TYPES.map((t) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: t, children: import_video_editor6.VIDEO_TRANSITION_LABELS[t] }, t))
|
|
682
806
|
]
|
|
683
807
|
}
|
|
684
808
|
) }),
|
|
685
|
-
transition && /* @__PURE__ */ (0,
|
|
809
|
+
transition && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Length (ms)", name: "transitionDuration", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
686
810
|
Commit,
|
|
687
811
|
{
|
|
688
812
|
type: "number",
|
|
@@ -697,7 +821,7 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
697
821
|
}
|
|
698
822
|
}
|
|
699
823
|
) }),
|
|
700
|
-
transition && (0,
|
|
824
|
+
transition && (0, import_video_editor6.videoTransitionHasDirection)(transition.type) && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Direction", name: "transitionDirection", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
701
825
|
"select",
|
|
702
826
|
{
|
|
703
827
|
className: field,
|
|
@@ -705,41 +829,41 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
705
829
|
disabled,
|
|
706
830
|
"aria-label": "Transition direction",
|
|
707
831
|
onChange: (e) => controller.apply({ op: "set_transition", clip: clip.id, transition: { ...transition, direction: e.target.value } }, "Change transition direction"),
|
|
708
|
-
children:
|
|
832
|
+
children: import_video_editor6.VIDEO_WIPE_DIRECTIONS.map((d) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: d, children: d }, d))
|
|
709
833
|
}
|
|
710
834
|
) }),
|
|
711
|
-
transition && /* @__PURE__ */ (0,
|
|
835
|
+
transition && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Transition sound", name: "transitionAudioCurve", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
712
836
|
"select",
|
|
713
837
|
{
|
|
714
838
|
className: field,
|
|
715
|
-
value: (0,
|
|
839
|
+
value: (0, import_video_editor6.videoTransitionAudioCurve)(transition),
|
|
716
840
|
disabled,
|
|
717
841
|
"aria-label": "Transition sound curve",
|
|
718
842
|
onChange: (e) => {
|
|
719
843
|
const { audioCurve: _, ...rest } = transition;
|
|
720
844
|
const curve = e.target.value;
|
|
721
|
-
const next = curve === (0,
|
|
845
|
+
const next = curve === (0, import_video_editor6.videoTransitionAudioCurve)(rest) ? rest : { ...rest, audioCurve: curve };
|
|
722
846
|
controller.apply({ op: "set_transition", clip: clip.id, transition: next }, "Change transition sound");
|
|
723
847
|
},
|
|
724
|
-
children:
|
|
848
|
+
children: import_video_editor6.VIDEO_FADE_CURVES.map((c) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: c, children: import_video_editor6.VIDEO_FADE_CURVE_LABELS[c] }, c))
|
|
725
849
|
}
|
|
726
850
|
) })
|
|
727
851
|
] });
|
|
728
852
|
}
|
|
729
853
|
|
|
730
854
|
// src/video-editor/MediaBin.tsx
|
|
731
|
-
var
|
|
732
|
-
var
|
|
733
|
-
var
|
|
855
|
+
var import_react6 = require("react");
|
|
856
|
+
var import_video_editor7 = require("@pixerate/editor/video-editor");
|
|
857
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
734
858
|
var btn3 = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
|
|
735
|
-
function MediaBin({ controller, editable = true, fileToUrl, probe =
|
|
859
|
+
function MediaBin({ controller, editable = true, fileToUrl, probe = import_video_editor7.probeVideoMedia, className = "" }) {
|
|
736
860
|
const { project, readOnly } = useVideoEditorState(controller);
|
|
737
861
|
const canEdit = editable && !readOnly;
|
|
738
|
-
const [url, setUrl] = (0,
|
|
739
|
-
const [urlKind, setUrlKind] = (0,
|
|
740
|
-
const [busy, setBusy] = (0,
|
|
741
|
-
const [problem, setProblem] = (0,
|
|
742
|
-
const [over, setOver] = (0,
|
|
862
|
+
const [url, setUrl] = (0, import_react6.useState)("");
|
|
863
|
+
const [urlKind, setUrlKind] = (0, import_react6.useState)("");
|
|
864
|
+
const [busy, setBusy] = (0, import_react6.useState)(0);
|
|
865
|
+
const [problem, setProblem] = (0, import_react6.useState)(null);
|
|
866
|
+
const [over, setOver] = (0, import_react6.useState)(false);
|
|
743
867
|
const addSource = async (src, kind, name) => {
|
|
744
868
|
setBusy((n) => n + 1);
|
|
745
869
|
try {
|
|
@@ -753,7 +877,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
|
|
|
753
877
|
const addFiles = async (files) => {
|
|
754
878
|
setProblem(null);
|
|
755
879
|
for (const file of Array.from(files ?? [])) {
|
|
756
|
-
const kind = (0,
|
|
880
|
+
const kind = (0, import_video_editor7.videoAssetKindOf)(file.type || file.name);
|
|
757
881
|
if (!kind) {
|
|
758
882
|
setProblem(`"${file.name}" is not a video, audio or image file.`);
|
|
759
883
|
continue;
|
|
@@ -765,7 +889,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
|
|
|
765
889
|
const addUrl = async () => {
|
|
766
890
|
const src = url.trim();
|
|
767
891
|
if (!src) return;
|
|
768
|
-
const kind = urlKind || (0,
|
|
892
|
+
const kind = urlKind || (0, import_video_editor7.videoAssetKindOf)(src.replace(/[?#].*$/, ""));
|
|
769
893
|
if (!kind) {
|
|
770
894
|
setProblem(`Can't tell whether "${src}" is video, audio or an image; choose its kind.`);
|
|
771
895
|
return;
|
|
@@ -775,7 +899,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
|
|
|
775
899
|
setUrl("");
|
|
776
900
|
};
|
|
777
901
|
const addToTimeline = (asset) => {
|
|
778
|
-
const edit = (0,
|
|
902
|
+
const edit = (0, import_video_editor7.videoAssetDropEdit)(project, asset.id);
|
|
779
903
|
if (!edit) {
|
|
780
904
|
setProblem(`There is no unlocked ${asset.kind === "audio" ? "audio" : "video"} track to put "${asset.name ?? asset.id}" on.`);
|
|
781
905
|
return;
|
|
@@ -787,7 +911,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
|
|
|
787
911
|
if (added) controller.select([added]);
|
|
788
912
|
}
|
|
789
913
|
};
|
|
790
|
-
return /* @__PURE__ */ (0,
|
|
914
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
791
915
|
"div",
|
|
792
916
|
{
|
|
793
917
|
className: `flex flex-col gap-2 rounded ${over ? "ring-2 ring-rose-500/60" : ""} ${className}`,
|
|
@@ -805,20 +929,20 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
|
|
|
805
929
|
void addFiles(e.dataTransfer.files);
|
|
806
930
|
},
|
|
807
931
|
children: [
|
|
808
|
-
/* @__PURE__ */ (0,
|
|
809
|
-
project.assets.length ? /* @__PURE__ */ (0,
|
|
810
|
-
/* @__PURE__ */ (0,
|
|
811
|
-
/* @__PURE__ */ (0,
|
|
812
|
-
asset.duration !== void 0 && /* @__PURE__ */ (0,
|
|
813
|
-
editable && /* @__PURE__ */ (0,
|
|
814
|
-
] }, asset.id)) }) : /* @__PURE__ */ (0,
|
|
932
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "text-xs font-medium", children: "Media" }),
|
|
933
|
+
project.assets.length ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ul", { className: "flex flex-col gap-1", "aria-label": "Media assets", children: project.assets.map((asset) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("li", { className: "flex items-center gap-2 text-xs", "data-asset": asset.id, children: [
|
|
934
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "rounded bg-muted px-1 text-[10px] uppercase text-muted-foreground", "data-asset-kind": "", children: asset.kind }),
|
|
935
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "min-w-0 flex-1 truncate", title: asset.src, children: asset.name ?? asset.id }),
|
|
936
|
+
asset.duration !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "tabular-nums text-muted-foreground", children: (0, import_video_editor7.formatVideoSeconds)(asset.duration) }),
|
|
937
|
+
editable && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: btn3, disabled: !canEdit, onClick: () => addToTimeline(asset), "data-add-to-timeline": "", children: "Add to timeline" })
|
|
938
|
+
] }, asset.id)) }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "text-xs text-muted-foreground", children: [
|
|
815
939
|
"No media yet.",
|
|
816
940
|
editable ? " Choose or drop files, or add a URL." : ""
|
|
817
941
|
] }),
|
|
818
|
-
editable && /* @__PURE__ */ (0,
|
|
819
|
-
/* @__PURE__ */ (0,
|
|
942
|
+
editable && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
|
|
943
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { className: "flex flex-col gap-1 rounded border border-dashed border-border p-2 text-xs text-muted-foreground", children: [
|
|
820
944
|
"Choose or drop video, audio or image files",
|
|
821
|
-
/* @__PURE__ */ (0,
|
|
945
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
822
946
|
"input",
|
|
823
947
|
{
|
|
824
948
|
type: "file",
|
|
@@ -835,7 +959,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
|
|
|
835
959
|
}
|
|
836
960
|
)
|
|
837
961
|
] }),
|
|
838
|
-
/* @__PURE__ */ (0,
|
|
962
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
839
963
|
"form",
|
|
840
964
|
{
|
|
841
965
|
className: "flex items-center gap-1",
|
|
@@ -844,7 +968,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
|
|
|
844
968
|
void addUrl();
|
|
845
969
|
},
|
|
846
970
|
children: [
|
|
847
|
-
/* @__PURE__ */ (0,
|
|
971
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
848
972
|
"input",
|
|
849
973
|
{
|
|
850
974
|
className: "min-w-0 flex-1 rounded border border-border bg-transparent px-1 py-0.5 text-xs",
|
|
@@ -855,49 +979,49 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
|
|
|
855
979
|
onChange: (e) => setUrl(e.target.value)
|
|
856
980
|
}
|
|
857
981
|
),
|
|
858
|
-
/* @__PURE__ */ (0,
|
|
859
|
-
/* @__PURE__ */ (0,
|
|
860
|
-
/* @__PURE__ */ (0,
|
|
861
|
-
/* @__PURE__ */ (0,
|
|
862
|
-
/* @__PURE__ */ (0,
|
|
982
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("select", { className: "rounded border border-border bg-transparent px-1 py-0.5 text-xs", value: urlKind, disabled: !canEdit, "aria-label": "Media kind", onChange: (e) => setUrlKind(e.target.value), children: [
|
|
983
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "", children: "Auto" }),
|
|
984
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "video", children: "Video" }),
|
|
985
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "audio", children: "Audio" }),
|
|
986
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "image", children: "Image" })
|
|
863
987
|
] }),
|
|
864
|
-
/* @__PURE__ */ (0,
|
|
988
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "submit", className: btn3, disabled: !canEdit || !url.trim(), "data-add-url": "", children: "Add" })
|
|
865
989
|
]
|
|
866
990
|
}
|
|
867
991
|
)
|
|
868
992
|
] }),
|
|
869
|
-
busy > 0 && /* @__PURE__ */ (0,
|
|
870
|
-
problem && /* @__PURE__ */ (0,
|
|
993
|
+
busy > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-xs text-muted-foreground", children: "Reading media\u2026" }),
|
|
994
|
+
problem && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-xs text-red-500", role: "alert", "data-media-error": "", children: problem })
|
|
871
995
|
]
|
|
872
996
|
}
|
|
873
997
|
);
|
|
874
998
|
}
|
|
875
999
|
|
|
876
1000
|
// src/video-editor/TranscriptPanel.tsx
|
|
877
|
-
var
|
|
878
|
-
var
|
|
879
|
-
var
|
|
1001
|
+
var import_react7 = __toESM(require("react"), 1);
|
|
1002
|
+
var import_video_editor8 = require("@pixerate/editor/video-editor");
|
|
1003
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
880
1004
|
var btn4 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
|
|
881
1005
|
function TranscriptPanel({ controller, track, editable = true, className = "" }) {
|
|
882
1006
|
const state = useVideoEditorState(controller);
|
|
883
1007
|
const { project, time, readOnly } = state;
|
|
884
1008
|
const canEdit = editable && !readOnly;
|
|
885
|
-
const [anchor, setAnchor] = (0,
|
|
886
|
-
const [focus, setFocus] = (0,
|
|
887
|
-
const [message, setMessage] = (0,
|
|
888
|
-
const words = (0,
|
|
1009
|
+
const [anchor, setAnchor] = (0, import_react7.useState)(null);
|
|
1010
|
+
const [focus, setFocus] = (0, import_react7.useState)(null);
|
|
1011
|
+
const [message, setMessage] = (0, import_react7.useState)(null);
|
|
1012
|
+
const words = (0, import_react7.useMemo)(() => {
|
|
889
1013
|
try {
|
|
890
|
-
return (0,
|
|
1014
|
+
return (0, import_video_editor8.videoTimelineTranscript)(project, { track });
|
|
891
1015
|
} catch {
|
|
892
1016
|
return [];
|
|
893
1017
|
}
|
|
894
1018
|
}, [project, track]);
|
|
895
|
-
const current = (0,
|
|
1019
|
+
const current = (0, import_video_editor8.videoWordAt)(words, time);
|
|
896
1020
|
const selected = anchor !== null && focus !== null && anchor < words.length && focus < words.length ? [Math.min(anchor, focus), Math.max(anchor, focus)] : null;
|
|
897
1021
|
const sound = project.assets.filter((a) => a.kind !== "image");
|
|
898
1022
|
const cut = (ranges, label, none) => {
|
|
899
1023
|
if (!ranges.length) return setMessage(none);
|
|
900
|
-
const r = controller.apply((0,
|
|
1024
|
+
const r = controller.apply((0, import_video_editor8.videoRemoveRangesEdits)(ranges), label);
|
|
901
1025
|
setMessage(r.ok ? null : r.error);
|
|
902
1026
|
setAnchor(null);
|
|
903
1027
|
setFocus(null);
|
|
@@ -905,15 +1029,15 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
|
|
|
905
1029
|
const load = async (assetId, file) => {
|
|
906
1030
|
if (!file) return;
|
|
907
1031
|
try {
|
|
908
|
-
const r = controller.apply((0,
|
|
1032
|
+
const r = controller.apply((0, import_video_editor8.videoTranscriptEdit)(assetId, await file.text()), "Attach transcript");
|
|
909
1033
|
setMessage(r.ok ? null : r.error);
|
|
910
1034
|
} catch (e) {
|
|
911
1035
|
setMessage(e instanceof Error ? e.message : String(e));
|
|
912
1036
|
}
|
|
913
1037
|
};
|
|
914
|
-
return /* @__PURE__ */ (0,
|
|
915
|
-
words.length > 0 ? /* @__PURE__ */ (0,
|
|
916
|
-
/* @__PURE__ */ (0,
|
|
1038
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: `flex flex-col gap-2 text-xs ${className}`, "data-transcript-panel": "", children: [
|
|
1039
|
+
words.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
|
|
1040
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
917
1041
|
"div",
|
|
918
1042
|
{
|
|
919
1043
|
className: "max-h-48 overflow-y-auto leading-6",
|
|
@@ -924,13 +1048,13 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
|
|
|
924
1048
|
onKeyDown: (e) => {
|
|
925
1049
|
if (canEdit && selected && (e.key === "Delete" || e.key === "Backspace")) {
|
|
926
1050
|
e.preventDefault();
|
|
927
|
-
cut([(0,
|
|
1051
|
+
cut([(0, import_video_editor8.videoWordsRange)(words, selected[0], selected[1])], "Cut words", "Select words to cut.");
|
|
928
1052
|
}
|
|
929
1053
|
},
|
|
930
1054
|
children: words.map((w, i) => {
|
|
931
1055
|
const inSelection = selected !== null && i >= selected[0] && i <= selected[1];
|
|
932
|
-
return /* @__PURE__ */ (0,
|
|
933
|
-
/* @__PURE__ */ (0,
|
|
1056
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react7.default.Fragment, { children: [
|
|
1057
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
934
1058
|
"span",
|
|
935
1059
|
{
|
|
936
1060
|
role: "option",
|
|
@@ -956,51 +1080,52 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
|
|
|
956
1080
|
})
|
|
957
1081
|
}
|
|
958
1082
|
),
|
|
959
|
-
canEdit && /* @__PURE__ */ (0,
|
|
960
|
-
/* @__PURE__ */ (0,
|
|
961
|
-
/* @__PURE__ */ (0,
|
|
962
|
-
/* @__PURE__ */ (0,
|
|
1083
|
+
canEdit && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex flex-wrap gap-1", children: [
|
|
1084
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: btn4, disabled: !selected, onClick: () => selected && cut([(0, import_video_editor8.videoWordsRange)(words, selected[0], selected[1])], "Cut words", "Select words to cut."), children: "Delete selected words" }),
|
|
1085
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: btn4, onClick: () => cut((0, import_video_editor8.findVideoFillers)(words), "Remove fillers", "No filler words found."), children: "Remove fillers" }),
|
|
1086
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: btn4, onClick: () => cut((0, import_video_editor8.findVideoTranscriptPauses)(words, { fps: project.fps }), "Tighten pauses", "No long pauses found."), children: "Tighten pauses" })
|
|
963
1087
|
] })
|
|
964
|
-
] }) : /* @__PURE__ */ (0,
|
|
965
|
-
canEdit && sound.length > 0 && /* @__PURE__ */ (0,
|
|
966
|
-
/* @__PURE__ */ (0,
|
|
967
|
-
/* @__PURE__ */ (0,
|
|
968
|
-
/* @__PURE__ */ (0,
|
|
1088
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "text-muted-foreground", "data-transcript-empty": "", children: "No transcribed media on the timeline. Load a transcript (Whisper JSON, .srt or .vtt) for a video or audio asset to edit by text." }),
|
|
1089
|
+
canEdit && sound.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("details", { children: [
|
|
1090
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("summary", { className: "cursor-pointer text-muted-foreground", children: "Transcripts" }),
|
|
1091
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("ul", { className: "mt-1 space-y-1", children: sound.map((a) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("li", { className: "flex items-center gap-2", children: [
|
|
1092
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "min-w-0 flex-1 truncate", children: [
|
|
969
1093
|
a.name ?? a.id,
|
|
970
1094
|
" ",
|
|
971
1095
|
a.transcript ? `\xB7 ${a.transcript.length} words` : ""
|
|
972
1096
|
] }),
|
|
973
|
-
/* @__PURE__ */ (0,
|
|
1097
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("input", { type: "file", accept: ".json,.srt,.vtt,application/json,text/vtt", "aria-label": `Load a transcript for ${a.name ?? a.id}`, className: "w-40 text-[10px]", onChange: (e) => void load(a.id, e.target.files?.[0]) })
|
|
974
1098
|
] }, a.id)) })
|
|
975
1099
|
] }),
|
|
976
|
-
message && /* @__PURE__ */ (0,
|
|
1100
|
+
message && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "text-red-500", role: "alert", "data-transcript-error": "", children: message })
|
|
977
1101
|
] });
|
|
978
1102
|
}
|
|
979
1103
|
|
|
980
1104
|
// src/video-editor/VideoEditor.tsx
|
|
981
|
-
var
|
|
982
|
-
var
|
|
983
|
-
function VideoEditor({ controller: given, options, editable = true, pxPerSecond, onPxPerSecondChange, resolution, showIssues = true, showTranscript = true, className = "" }) {
|
|
1105
|
+
var import_react8 = require("react");
|
|
1106
|
+
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
1107
|
+
function VideoEditor({ controller: given, options, editable = true, pxPerSecond, onPxPerSecondChange, resolution, showIssues = true, showTranscript = true, showScopes = false, className = "" }) {
|
|
984
1108
|
const { controller, state } = useVideoEditor(given ?? options ?? {});
|
|
985
|
-
(0,
|
|
1109
|
+
(0, import_react8.useEffect)(() => {
|
|
986
1110
|
if (editable) return;
|
|
987
1111
|
const was = controller.getState().readOnly;
|
|
988
1112
|
controller.setReadOnly(true);
|
|
989
1113
|
return () => controller.setReadOnly(was);
|
|
990
1114
|
}, [controller, editable]);
|
|
991
1115
|
const { issues } = state;
|
|
992
|
-
const issueList = showIssues && issues.length > 0 && /* @__PURE__ */ (0,
|
|
993
|
-
return /* @__PURE__ */ (0,
|
|
994
|
-
/* @__PURE__ */ (0,
|
|
995
|
-
editable && /* @__PURE__ */ (0,
|
|
996
|
-
/* @__PURE__ */ (0,
|
|
997
|
-
/* @__PURE__ */ (0,
|
|
998
|
-
/* @__PURE__ */ (0,
|
|
1116
|
+
const issueList = showIssues && issues.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("ul", { className: "space-y-0.5 text-xs", "aria-label": "Video issues", "data-video-issues": "", children: issues.map((issue, i) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("li", { className: issue.severity === "error" ? "text-red-500" : issue.severity === "warning" ? "text-amber-500" : "text-muted-foreground", "data-issue": issue.code, children: issue.message }, i)) });
|
|
1117
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: `flex flex-col gap-3 ${className}`, "data-video-editor": "", children: [
|
|
1118
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: `grid gap-3 ${editable ? "lg:grid-cols-[16rem_minmax(0,1fr)_18rem]" : ""}`, children: [
|
|
1119
|
+
editable && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(MediaBin, { controller, className: "min-w-0" }),
|
|
1120
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex min-w-0 flex-col gap-2", children: [
|
|
1121
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(VideoPreview, { controller, resolution, className: "rounded-md border border-border" }),
|
|
1122
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(VideoTransport, { controller, editable }),
|
|
1123
|
+
showScopes && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(VideoScopes, { controller })
|
|
999
1124
|
] }),
|
|
1000
|
-
editable && /* @__PURE__ */ (0,
|
|
1125
|
+
editable && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ClipInspector, { controller, className: "min-w-0" })
|
|
1001
1126
|
] }),
|
|
1002
|
-
/* @__PURE__ */ (0,
|
|
1003
|
-
showTranscript && /* @__PURE__ */ (0,
|
|
1127
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(VideoTimeline, { controller, editable, pxPerSecond, onPxPerSecondChange }),
|
|
1128
|
+
showTranscript && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(TranscriptPanel, { controller, editable }),
|
|
1004
1129
|
issueList
|
|
1005
1130
|
] });
|
|
1006
1131
|
}
|
|
@@ -1011,6 +1136,7 @@ function VideoEditor({ controller: given, options, editable = true, pxPerSecond,
|
|
|
1011
1136
|
TranscriptPanel,
|
|
1012
1137
|
VideoEditor,
|
|
1013
1138
|
VideoPreview,
|
|
1139
|
+
VideoScopes,
|
|
1014
1140
|
VideoTimeline,
|
|
1015
1141
|
VideoTransport,
|
|
1016
1142
|
useVideoEditor,
|