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