@pixerate/editor-react 0.56.0 → 0.57.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,17 @@ 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_video_editor6 = require("@pixerate/editor/video-editor");
|
|
522
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
454
523
|
var btn2 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
|
|
455
524
|
var field = "w-full rounded border border-border bg-transparent px-1 py-0.5 text-xs disabled:opacity-50";
|
|
456
525
|
var DEFAULT_TRANSITION = 500;
|
|
@@ -464,7 +533,7 @@ function Commit({
|
|
|
464
533
|
if (raw !== value) onCommit(raw);
|
|
465
534
|
};
|
|
466
535
|
if (multiline)
|
|
467
|
-
return /* @__PURE__ */ (0,
|
|
536
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
468
537
|
"textarea",
|
|
469
538
|
{
|
|
470
539
|
className: field,
|
|
@@ -476,11 +545,11 @@ function Commit({
|
|
|
476
545
|
},
|
|
477
546
|
value
|
|
478
547
|
);
|
|
479
|
-
return /* @__PURE__ */ (0,
|
|
548
|
+
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);
|
|
480
549
|
}
|
|
481
550
|
function Row({ label, name, children }) {
|
|
482
|
-
return /* @__PURE__ */ (0,
|
|
483
|
-
/* @__PURE__ */ (0,
|
|
551
|
+
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: [
|
|
552
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-muted-foreground", children: label }),
|
|
484
553
|
children
|
|
485
554
|
] });
|
|
486
555
|
}
|
|
@@ -488,8 +557,8 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
488
557
|
const state = useVideoEditorState(controller);
|
|
489
558
|
const { project, selection, readOnly, time } = state;
|
|
490
559
|
if (selection.length !== 1)
|
|
491
|
-
return /* @__PURE__ */ (0,
|
|
492
|
-
const at = (0,
|
|
560
|
+
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." });
|
|
561
|
+
const at = (0, import_video_editor6.findVideoClip)(project, selection[0]);
|
|
493
562
|
if (!at) return null;
|
|
494
563
|
const clip = at.clip;
|
|
495
564
|
const disabled = readOnly || !!at.track.locked;
|
|
@@ -505,24 +574,24 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
505
574
|
};
|
|
506
575
|
const text = (key, label) => {
|
|
507
576
|
const value = clip[key];
|
|
508
|
-
return /* @__PURE__ */ (0,
|
|
577
|
+
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
578
|
};
|
|
510
579
|
const number = (key, label, attrs = {}) => {
|
|
511
580
|
const value = clip[key];
|
|
512
|
-
return /* @__PURE__ */ (0,
|
|
581
|
+
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
582
|
};
|
|
514
583
|
const transition = clip.transition;
|
|
515
|
-
return /* @__PURE__ */ (0,
|
|
516
|
-
/* @__PURE__ */ (0,
|
|
517
|
-
/* @__PURE__ */ (0,
|
|
518
|
-
/* @__PURE__ */ (0,
|
|
519
|
-
at.track.locked && /* @__PURE__ */ (0,
|
|
584
|
+
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: [
|
|
585
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-2 text-xs", children: [
|
|
586
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "font-medium", children: clip.type === "media" ? "Media clip" : clip.type === "text" ? "Title" : "Colour clip" }),
|
|
587
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("code", { className: "text-muted-foreground", children: clip.id }),
|
|
588
|
+
at.track.locked && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-amber-500", children: "locked track" })
|
|
520
589
|
] }),
|
|
521
|
-
/* @__PURE__ */ (0,
|
|
522
|
-
/* @__PURE__ */ (0,
|
|
523
|
-
/* @__PURE__ */ (0,
|
|
524
|
-
(0,
|
|
525
|
-
/* @__PURE__ */ (0,
|
|
590
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Name", name: "name", children: text("name", "Name") }),
|
|
591
|
+
/* @__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) }) }),
|
|
592
|
+
/* @__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) }) }),
|
|
593
|
+
(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: [
|
|
594
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
526
595
|
Commit,
|
|
527
596
|
{
|
|
528
597
|
type: "number",
|
|
@@ -536,12 +605,12 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
536
605
|
onCommit: (raw) => {
|
|
537
606
|
const n = Number(raw);
|
|
538
607
|
if (raw.trim() && !Number.isFinite(n)) return;
|
|
539
|
-
const edit = (0,
|
|
608
|
+
const edit = (0, import_video_editor6.videoKeyframeValueEdit)(project, clip.id, row.property, time, raw.trim() ? n : void 0);
|
|
540
609
|
if (edit) controller.apply(edit, row.keyed ? `Keyframe ${row.label.toLowerCase()}` : `Change ${row.label.toLowerCase()}`);
|
|
541
610
|
}
|
|
542
611
|
}
|
|
543
612
|
),
|
|
544
|
-
/* @__PURE__ */ (0,
|
|
613
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
545
614
|
"button",
|
|
546
615
|
{
|
|
547
616
|
type: "button",
|
|
@@ -552,17 +621,17 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
552
621
|
disabled,
|
|
553
622
|
"data-keyframe-toggle": row.property,
|
|
554
623
|
onClick: () => {
|
|
555
|
-
const edit = (0,
|
|
624
|
+
const edit = (0, import_video_editor6.videoKeyframeToggleEdit)(project, clip.id, row.property, time);
|
|
556
625
|
if (edit) controller.apply(edit, `${row.onKey ? "Remove" : "Add"} ${row.label.toLowerCase()} keyframe`);
|
|
557
626
|
},
|
|
558
627
|
children: row.onKey ? "\u25C6" : "\u25C7"
|
|
559
628
|
}
|
|
560
629
|
),
|
|
561
|
-
row.keyed && /* @__PURE__ */ (0,
|
|
630
|
+
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
631
|
] }) }, row.property)),
|
|
563
|
-
at.track.kind === "video" && /* @__PURE__ */ (0,
|
|
564
|
-
at.track.kind === "video" && /* @__PURE__ */ (0,
|
|
565
|
-
/* @__PURE__ */ (0,
|
|
632
|
+
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)) }) }),
|
|
633
|
+
at.track.kind === "video" && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
|
|
634
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Look", name: "gradePreset", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
566
635
|
"select",
|
|
567
636
|
{
|
|
568
637
|
className: field,
|
|
@@ -570,16 +639,16 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
570
639
|
disabled,
|
|
571
640
|
"aria-label": "Apply a look",
|
|
572
641
|
onChange: (e) => {
|
|
573
|
-
const grade = (0,
|
|
642
|
+
const grade = (0, import_video_editor6.videoGradePreset)(e.target.value);
|
|
574
643
|
if (grade) controller.apply({ op: "update_clip", clip: clip.id, set: { grade } }, `Apply ${e.target.value} look`);
|
|
575
644
|
},
|
|
576
645
|
children: [
|
|
577
|
-
/* @__PURE__ */ (0,
|
|
578
|
-
|
|
646
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: clip.grade ? "Graded" : "None" }),
|
|
647
|
+
import_video_editor6.VIDEO_GRADE_PRESET_NAMES.map((name) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: name, children: name.replace(/-/g, " ") }, name))
|
|
579
648
|
]
|
|
580
649
|
}
|
|
581
650
|
) }),
|
|
582
|
-
|
|
651
|
+
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
652
|
Commit,
|
|
584
653
|
{
|
|
585
654
|
type: "number",
|
|
@@ -593,13 +662,13 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
593
662
|
onCommit: (raw) => {
|
|
594
663
|
const n = raw.trim() ? Number(raw) : 0;
|
|
595
664
|
if (!Number.isFinite(n)) return;
|
|
596
|
-
const grade = (0,
|
|
665
|
+
const grade = (0, import_video_editor6.normalizeVideoGrade)({ ...clip.grade, [c.key]: n });
|
|
597
666
|
set("grade", grade, `Change ${c.label.toLowerCase()}`);
|
|
598
667
|
}
|
|
599
668
|
}
|
|
600
669
|
) }, c.key))
|
|
601
670
|
] }),
|
|
602
|
-
at.track.kind === "video" && ["animateIn", "animateOut"].map((key) => /* @__PURE__ */ (0,
|
|
671
|
+
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
672
|
"select",
|
|
604
673
|
{
|
|
605
674
|
className: field,
|
|
@@ -611,35 +680,35 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
611
680
|
set(key, effect ? { ...clip[key], effect } : void 0, key === "animateIn" ? "Change entrance" : "Change exit");
|
|
612
681
|
},
|
|
613
682
|
children: [
|
|
614
|
-
/* @__PURE__ */ (0,
|
|
615
|
-
|
|
683
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: "None" }),
|
|
684
|
+
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
685
|
]
|
|
617
686
|
}
|
|
618
687
|
) }, key)),
|
|
619
|
-
/* @__PURE__ */ (0,
|
|
620
|
-
/* @__PURE__ */ (0,
|
|
621
|
-
clip.type === "media" && /* @__PURE__ */ (0,
|
|
622
|
-
/* @__PURE__ */ (0,
|
|
623
|
-
/* @__PURE__ */ (0,
|
|
624
|
-
/* @__PURE__ */ (0,
|
|
688
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Fade in (ms)", name: "fadeIn", children: number("fadeIn", "Fade in", { min: 0, step: 100 }) }),
|
|
689
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Fade out (ms)", name: "fadeOut", children: number("fadeOut", "Fade out", { min: 0, step: 100 }) }),
|
|
690
|
+
clip.type === "media" && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
|
|
691
|
+
/* @__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: [
|
|
692
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-muted-foreground", children: "ramped" }),
|
|
693
|
+
/* @__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
694
|
] }) : 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,
|
|
695
|
+
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: [
|
|
696
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
628
697
|
"button",
|
|
629
698
|
{
|
|
630
699
|
type: "button",
|
|
631
700
|
className: btn2,
|
|
632
701
|
disabled: disabled || time < clip.start || time > clip.start + clip.duration,
|
|
633
702
|
title: "Hold the frame at the playhead for a second, pushing later clips along",
|
|
634
|
-
onClick: () => controller.apply((0,
|
|
703
|
+
onClick: () => controller.apply((0, import_video_editor6.videoFreezeFrameEdit)(clip, time - clip.start, 1e3), "Freeze frame"),
|
|
635
704
|
children: "Freeze 1 s here"
|
|
636
705
|
}
|
|
637
706
|
),
|
|
638
|
-
/* @__PURE__ */ (0,
|
|
707
|
+
/* @__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
708
|
] }) }),
|
|
640
|
-
/* @__PURE__ */ (0,
|
|
709
|
+
/* @__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
710
|
["fadeInCurve", "fadeOutCurve"].map(
|
|
642
|
-
(key) => (key === "fadeInCurve" ? clip.fadeIn : clip.fadeOut) ? /* @__PURE__ */ (0,
|
|
711
|
+
(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
712
|
"select",
|
|
644
713
|
{
|
|
645
714
|
className: field,
|
|
@@ -650,19 +719,19 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
650
719
|
const curve = e.target.value;
|
|
651
720
|
set(key, curve === "linear" ? void 0 : curve, key === "fadeInCurve" ? "Change fade in curve" : "Change fade out curve");
|
|
652
721
|
},
|
|
653
|
-
children:
|
|
722
|
+
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
723
|
}
|
|
655
724
|
) }, key) : null
|
|
656
725
|
)
|
|
657
726
|
] }),
|
|
658
|
-
clip.type === "text" && /* @__PURE__ */ (0,
|
|
659
|
-
/* @__PURE__ */ (0,
|
|
660
|
-
/* @__PURE__ */ (0,
|
|
661
|
-
/* @__PURE__ */ (0,
|
|
662
|
-
/* @__PURE__ */ (0,
|
|
727
|
+
clip.type === "text" && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
|
|
728
|
+
/* @__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") }) }),
|
|
729
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Colour", name: "color", children: text("color", "Colour") }),
|
|
730
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Font size", name: "fontSize", children: number("fontSize", "Font size", { min: 1, step: 2 }) }),
|
|
731
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Background", name: "background", children: text("background", "Background") })
|
|
663
732
|
] }),
|
|
664
|
-
clip.type === "color" && /* @__PURE__ */ (0,
|
|
665
|
-
/* @__PURE__ */ (0,
|
|
733
|
+
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") }) }),
|
|
734
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Transition in", name: "transition", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
666
735
|
"select",
|
|
667
736
|
{
|
|
668
737
|
className: field,
|
|
@@ -677,12 +746,12 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
677
746
|
);
|
|
678
747
|
},
|
|
679
748
|
children: [
|
|
680
|
-
/* @__PURE__ */ (0,
|
|
681
|
-
|
|
749
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: "None" }),
|
|
750
|
+
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
751
|
]
|
|
683
752
|
}
|
|
684
753
|
) }),
|
|
685
|
-
transition && /* @__PURE__ */ (0,
|
|
754
|
+
transition && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Length (ms)", name: "transitionDuration", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
686
755
|
Commit,
|
|
687
756
|
{
|
|
688
757
|
type: "number",
|
|
@@ -697,7 +766,7 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
697
766
|
}
|
|
698
767
|
}
|
|
699
768
|
) }),
|
|
700
|
-
transition && (0,
|
|
769
|
+
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
770
|
"select",
|
|
702
771
|
{
|
|
703
772
|
className: field,
|
|
@@ -705,41 +774,41 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
705
774
|
disabled,
|
|
706
775
|
"aria-label": "Transition direction",
|
|
707
776
|
onChange: (e) => controller.apply({ op: "set_transition", clip: clip.id, transition: { ...transition, direction: e.target.value } }, "Change transition direction"),
|
|
708
|
-
children:
|
|
777
|
+
children: import_video_editor6.VIDEO_WIPE_DIRECTIONS.map((d) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: d, children: d }, d))
|
|
709
778
|
}
|
|
710
779
|
) }),
|
|
711
|
-
transition && /* @__PURE__ */ (0,
|
|
780
|
+
transition && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Transition sound", name: "transitionAudioCurve", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
712
781
|
"select",
|
|
713
782
|
{
|
|
714
783
|
className: field,
|
|
715
|
-
value: (0,
|
|
784
|
+
value: (0, import_video_editor6.videoTransitionAudioCurve)(transition),
|
|
716
785
|
disabled,
|
|
717
786
|
"aria-label": "Transition sound curve",
|
|
718
787
|
onChange: (e) => {
|
|
719
788
|
const { audioCurve: _, ...rest } = transition;
|
|
720
789
|
const curve = e.target.value;
|
|
721
|
-
const next = curve === (0,
|
|
790
|
+
const next = curve === (0, import_video_editor6.videoTransitionAudioCurve)(rest) ? rest : { ...rest, audioCurve: curve };
|
|
722
791
|
controller.apply({ op: "set_transition", clip: clip.id, transition: next }, "Change transition sound");
|
|
723
792
|
},
|
|
724
|
-
children:
|
|
793
|
+
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
794
|
}
|
|
726
795
|
) })
|
|
727
796
|
] });
|
|
728
797
|
}
|
|
729
798
|
|
|
730
799
|
// src/video-editor/MediaBin.tsx
|
|
731
|
-
var
|
|
732
|
-
var
|
|
733
|
-
var
|
|
800
|
+
var import_react5 = require("react");
|
|
801
|
+
var import_video_editor7 = require("@pixerate/editor/video-editor");
|
|
802
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
734
803
|
var btn3 = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
|
|
735
|
-
function MediaBin({ controller, editable = true, fileToUrl, probe =
|
|
804
|
+
function MediaBin({ controller, editable = true, fileToUrl, probe = import_video_editor7.probeVideoMedia, className = "" }) {
|
|
736
805
|
const { project, readOnly } = useVideoEditorState(controller);
|
|
737
806
|
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,
|
|
807
|
+
const [url, setUrl] = (0, import_react5.useState)("");
|
|
808
|
+
const [urlKind, setUrlKind] = (0, import_react5.useState)("");
|
|
809
|
+
const [busy, setBusy] = (0, import_react5.useState)(0);
|
|
810
|
+
const [problem, setProblem] = (0, import_react5.useState)(null);
|
|
811
|
+
const [over, setOver] = (0, import_react5.useState)(false);
|
|
743
812
|
const addSource = async (src, kind, name) => {
|
|
744
813
|
setBusy((n) => n + 1);
|
|
745
814
|
try {
|
|
@@ -753,7 +822,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
|
|
|
753
822
|
const addFiles = async (files) => {
|
|
754
823
|
setProblem(null);
|
|
755
824
|
for (const file of Array.from(files ?? [])) {
|
|
756
|
-
const kind = (0,
|
|
825
|
+
const kind = (0, import_video_editor7.videoAssetKindOf)(file.type || file.name);
|
|
757
826
|
if (!kind) {
|
|
758
827
|
setProblem(`"${file.name}" is not a video, audio or image file.`);
|
|
759
828
|
continue;
|
|
@@ -765,7 +834,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
|
|
|
765
834
|
const addUrl = async () => {
|
|
766
835
|
const src = url.trim();
|
|
767
836
|
if (!src) return;
|
|
768
|
-
const kind = urlKind || (0,
|
|
837
|
+
const kind = urlKind || (0, import_video_editor7.videoAssetKindOf)(src.replace(/[?#].*$/, ""));
|
|
769
838
|
if (!kind) {
|
|
770
839
|
setProblem(`Can't tell whether "${src}" is video, audio or an image; choose its kind.`);
|
|
771
840
|
return;
|
|
@@ -775,7 +844,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
|
|
|
775
844
|
setUrl("");
|
|
776
845
|
};
|
|
777
846
|
const addToTimeline = (asset) => {
|
|
778
|
-
const edit = (0,
|
|
847
|
+
const edit = (0, import_video_editor7.videoAssetDropEdit)(project, asset.id);
|
|
779
848
|
if (!edit) {
|
|
780
849
|
setProblem(`There is no unlocked ${asset.kind === "audio" ? "audio" : "video"} track to put "${asset.name ?? asset.id}" on.`);
|
|
781
850
|
return;
|
|
@@ -787,7 +856,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
|
|
|
787
856
|
if (added) controller.select([added]);
|
|
788
857
|
}
|
|
789
858
|
};
|
|
790
|
-
return /* @__PURE__ */ (0,
|
|
859
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
791
860
|
"div",
|
|
792
861
|
{
|
|
793
862
|
className: `flex flex-col gap-2 rounded ${over ? "ring-2 ring-rose-500/60" : ""} ${className}`,
|
|
@@ -805,20 +874,20 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
|
|
|
805
874
|
void addFiles(e.dataTransfer.files);
|
|
806
875
|
},
|
|
807
876
|
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,
|
|
877
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "text-xs font-medium", children: "Media" }),
|
|
878
|
+
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: [
|
|
879
|
+
/* @__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 }),
|
|
880
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "min-w-0 flex-1 truncate", title: asset.src, children: asset.name ?? asset.id }),
|
|
881
|
+
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) }),
|
|
882
|
+
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" })
|
|
883
|
+
] }, asset.id)) }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "text-xs text-muted-foreground", children: [
|
|
815
884
|
"No media yet.",
|
|
816
885
|
editable ? " Choose or drop files, or add a URL." : ""
|
|
817
886
|
] }),
|
|
818
|
-
editable && /* @__PURE__ */ (0,
|
|
819
|
-
/* @__PURE__ */ (0,
|
|
887
|
+
editable && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
|
|
888
|
+
/* @__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
889
|
"Choose or drop video, audio or image files",
|
|
821
|
-
/* @__PURE__ */ (0,
|
|
890
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
822
891
|
"input",
|
|
823
892
|
{
|
|
824
893
|
type: "file",
|
|
@@ -835,7 +904,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
|
|
|
835
904
|
}
|
|
836
905
|
)
|
|
837
906
|
] }),
|
|
838
|
-
/* @__PURE__ */ (0,
|
|
907
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
839
908
|
"form",
|
|
840
909
|
{
|
|
841
910
|
className: "flex items-center gap-1",
|
|
@@ -844,7 +913,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
|
|
|
844
913
|
void addUrl();
|
|
845
914
|
},
|
|
846
915
|
children: [
|
|
847
|
-
/* @__PURE__ */ (0,
|
|
916
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
848
917
|
"input",
|
|
849
918
|
{
|
|
850
919
|
className: "min-w-0 flex-1 rounded border border-border bg-transparent px-1 py-0.5 text-xs",
|
|
@@ -855,49 +924,49 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
|
|
|
855
924
|
onChange: (e) => setUrl(e.target.value)
|
|
856
925
|
}
|
|
857
926
|
),
|
|
858
|
-
/* @__PURE__ */ (0,
|
|
859
|
-
/* @__PURE__ */ (0,
|
|
860
|
-
/* @__PURE__ */ (0,
|
|
861
|
-
/* @__PURE__ */ (0,
|
|
862
|
-
/* @__PURE__ */ (0,
|
|
927
|
+
/* @__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: [
|
|
928
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "", children: "Auto" }),
|
|
929
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "video", children: "Video" }),
|
|
930
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "audio", children: "Audio" }),
|
|
931
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "image", children: "Image" })
|
|
863
932
|
] }),
|
|
864
|
-
/* @__PURE__ */ (0,
|
|
933
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "submit", className: btn3, disabled: !canEdit || !url.trim(), "data-add-url": "", children: "Add" })
|
|
865
934
|
]
|
|
866
935
|
}
|
|
867
936
|
)
|
|
868
937
|
] }),
|
|
869
|
-
busy > 0 && /* @__PURE__ */ (0,
|
|
870
|
-
problem && /* @__PURE__ */ (0,
|
|
938
|
+
busy > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-xs text-muted-foreground", children: "Reading media\u2026" }),
|
|
939
|
+
problem && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-xs text-red-500", role: "alert", "data-media-error": "", children: problem })
|
|
871
940
|
]
|
|
872
941
|
}
|
|
873
942
|
);
|
|
874
943
|
}
|
|
875
944
|
|
|
876
945
|
// src/video-editor/TranscriptPanel.tsx
|
|
877
|
-
var
|
|
878
|
-
var
|
|
879
|
-
var
|
|
946
|
+
var import_react6 = __toESM(require("react"), 1);
|
|
947
|
+
var import_video_editor8 = require("@pixerate/editor/video-editor");
|
|
948
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
880
949
|
var btn4 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
|
|
881
950
|
function TranscriptPanel({ controller, track, editable = true, className = "" }) {
|
|
882
951
|
const state = useVideoEditorState(controller);
|
|
883
952
|
const { project, time, readOnly } = state;
|
|
884
953
|
const canEdit = editable && !readOnly;
|
|
885
|
-
const [anchor, setAnchor] = (0,
|
|
886
|
-
const [focus, setFocus] = (0,
|
|
887
|
-
const [message, setMessage] = (0,
|
|
888
|
-
const words = (0,
|
|
954
|
+
const [anchor, setAnchor] = (0, import_react6.useState)(null);
|
|
955
|
+
const [focus, setFocus] = (0, import_react6.useState)(null);
|
|
956
|
+
const [message, setMessage] = (0, import_react6.useState)(null);
|
|
957
|
+
const words = (0, import_react6.useMemo)(() => {
|
|
889
958
|
try {
|
|
890
|
-
return (0,
|
|
959
|
+
return (0, import_video_editor8.videoTimelineTranscript)(project, { track });
|
|
891
960
|
} catch {
|
|
892
961
|
return [];
|
|
893
962
|
}
|
|
894
963
|
}, [project, track]);
|
|
895
|
-
const current = (0,
|
|
964
|
+
const current = (0, import_video_editor8.videoWordAt)(words, time);
|
|
896
965
|
const selected = anchor !== null && focus !== null && anchor < words.length && focus < words.length ? [Math.min(anchor, focus), Math.max(anchor, focus)] : null;
|
|
897
966
|
const sound = project.assets.filter((a) => a.kind !== "image");
|
|
898
967
|
const cut = (ranges, label, none) => {
|
|
899
968
|
if (!ranges.length) return setMessage(none);
|
|
900
|
-
const r = controller.apply((0,
|
|
969
|
+
const r = controller.apply((0, import_video_editor8.videoRemoveRangesEdits)(ranges), label);
|
|
901
970
|
setMessage(r.ok ? null : r.error);
|
|
902
971
|
setAnchor(null);
|
|
903
972
|
setFocus(null);
|
|
@@ -905,15 +974,15 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
|
|
|
905
974
|
const load = async (assetId, file) => {
|
|
906
975
|
if (!file) return;
|
|
907
976
|
try {
|
|
908
|
-
const r = controller.apply((0,
|
|
977
|
+
const r = controller.apply((0, import_video_editor8.videoTranscriptEdit)(assetId, await file.text()), "Attach transcript");
|
|
909
978
|
setMessage(r.ok ? null : r.error);
|
|
910
979
|
} catch (e) {
|
|
911
980
|
setMessage(e instanceof Error ? e.message : String(e));
|
|
912
981
|
}
|
|
913
982
|
};
|
|
914
|
-
return /* @__PURE__ */ (0,
|
|
915
|
-
words.length > 0 ? /* @__PURE__ */ (0,
|
|
916
|
-
/* @__PURE__ */ (0,
|
|
983
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: `flex flex-col gap-2 text-xs ${className}`, "data-transcript-panel": "", children: [
|
|
984
|
+
words.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
|
|
985
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
917
986
|
"div",
|
|
918
987
|
{
|
|
919
988
|
className: "max-h-48 overflow-y-auto leading-6",
|
|
@@ -924,13 +993,13 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
|
|
|
924
993
|
onKeyDown: (e) => {
|
|
925
994
|
if (canEdit && selected && (e.key === "Delete" || e.key === "Backspace")) {
|
|
926
995
|
e.preventDefault();
|
|
927
|
-
cut([(0,
|
|
996
|
+
cut([(0, import_video_editor8.videoWordsRange)(words, selected[0], selected[1])], "Cut words", "Select words to cut.");
|
|
928
997
|
}
|
|
929
998
|
},
|
|
930
999
|
children: words.map((w, i) => {
|
|
931
1000
|
const inSelection = selected !== null && i >= selected[0] && i <= selected[1];
|
|
932
|
-
return /* @__PURE__ */ (0,
|
|
933
|
-
/* @__PURE__ */ (0,
|
|
1001
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react6.default.Fragment, { children: [
|
|
1002
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
934
1003
|
"span",
|
|
935
1004
|
{
|
|
936
1005
|
role: "option",
|
|
@@ -956,51 +1025,52 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
|
|
|
956
1025
|
})
|
|
957
1026
|
}
|
|
958
1027
|
),
|
|
959
|
-
canEdit && /* @__PURE__ */ (0,
|
|
960
|
-
/* @__PURE__ */ (0,
|
|
961
|
-
/* @__PURE__ */ (0,
|
|
962
|
-
/* @__PURE__ */ (0,
|
|
1028
|
+
canEdit && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex flex-wrap gap-1", children: [
|
|
1029
|
+
/* @__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" }),
|
|
1030
|
+
/* @__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" }),
|
|
1031
|
+
/* @__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
1032
|
] })
|
|
964
|
-
] }) : /* @__PURE__ */ (0,
|
|
965
|
-
canEdit && sound.length > 0 && /* @__PURE__ */ (0,
|
|
966
|
-
/* @__PURE__ */ (0,
|
|
967
|
-
/* @__PURE__ */ (0,
|
|
968
|
-
/* @__PURE__ */ (0,
|
|
1033
|
+
] }) : /* @__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." }),
|
|
1034
|
+
canEdit && sound.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("details", { children: [
|
|
1035
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("summary", { className: "cursor-pointer text-muted-foreground", children: "Transcripts" }),
|
|
1036
|
+
/* @__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: [
|
|
1037
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "min-w-0 flex-1 truncate", children: [
|
|
969
1038
|
a.name ?? a.id,
|
|
970
1039
|
" ",
|
|
971
1040
|
a.transcript ? `\xB7 ${a.transcript.length} words` : ""
|
|
972
1041
|
] }),
|
|
973
|
-
/* @__PURE__ */ (0,
|
|
1042
|
+
/* @__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
1043
|
] }, a.id)) })
|
|
975
1044
|
] }),
|
|
976
|
-
message && /* @__PURE__ */ (0,
|
|
1045
|
+
message && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "text-red-500", role: "alert", "data-transcript-error": "", children: message })
|
|
977
1046
|
] });
|
|
978
1047
|
}
|
|
979
1048
|
|
|
980
1049
|
// 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 = "" }) {
|
|
1050
|
+
var import_react7 = require("react");
|
|
1051
|
+
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
1052
|
+
function VideoEditor({ controller: given, options, editable = true, pxPerSecond, onPxPerSecondChange, resolution, showIssues = true, showTranscript = true, showScopes = false, className = "" }) {
|
|
984
1053
|
const { controller, state } = useVideoEditor(given ?? options ?? {});
|
|
985
|
-
(0,
|
|
1054
|
+
(0, import_react7.useEffect)(() => {
|
|
986
1055
|
if (editable) return;
|
|
987
1056
|
const was = controller.getState().readOnly;
|
|
988
1057
|
controller.setReadOnly(true);
|
|
989
1058
|
return () => controller.setReadOnly(was);
|
|
990
1059
|
}, [controller, editable]);
|
|
991
1060
|
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,
|
|
1061
|
+
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)) });
|
|
1062
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: `flex flex-col gap-3 ${className}`, "data-video-editor": "", children: [
|
|
1063
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: `grid gap-3 ${editable ? "lg:grid-cols-[16rem_minmax(0,1fr)_18rem]" : ""}`, children: [
|
|
1064
|
+
editable && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(MediaBin, { controller, className: "min-w-0" }),
|
|
1065
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex min-w-0 flex-col gap-2", children: [
|
|
1066
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(VideoPreview, { controller, resolution, className: "rounded-md border border-border" }),
|
|
1067
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(VideoTransport, { controller, editable }),
|
|
1068
|
+
showScopes && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(VideoScopes, { controller })
|
|
999
1069
|
] }),
|
|
1000
|
-
editable && /* @__PURE__ */ (0,
|
|
1070
|
+
editable && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ClipInspector, { controller, className: "min-w-0" })
|
|
1001
1071
|
] }),
|
|
1002
|
-
/* @__PURE__ */ (0,
|
|
1003
|
-
showTranscript && /* @__PURE__ */ (0,
|
|
1072
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(VideoTimeline, { controller, editable, pxPerSecond, onPxPerSecondChange }),
|
|
1073
|
+
showTranscript && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(TranscriptPanel, { controller, editable }),
|
|
1004
1074
|
issueList
|
|
1005
1075
|
] });
|
|
1006
1076
|
}
|
|
@@ -1011,6 +1081,7 @@ function VideoEditor({ controller: given, options, editable = true, pxPerSecond,
|
|
|
1011
1081
|
TranscriptPanel,
|
|
1012
1082
|
VideoEditor,
|
|
1013
1083
|
VideoPreview,
|
|
1084
|
+
VideoScopes,
|
|
1014
1085
|
VideoTimeline,
|
|
1015
1086
|
VideoTransport,
|
|
1016
1087
|
useVideoEditor,
|