@pixerate/editor-react 0.48.0 → 0.49.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.
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/video-editor/index.ts
|
|
21
|
+
var video_editor_exports = {};
|
|
22
|
+
__export(video_editor_exports, {
|
|
23
|
+
ClipInspector: () => ClipInspector,
|
|
24
|
+
MediaBin: () => MediaBin,
|
|
25
|
+
VideoEditor: () => VideoEditor,
|
|
26
|
+
VideoPreview: () => VideoPreview,
|
|
27
|
+
VideoTimeline: () => VideoTimeline,
|
|
28
|
+
VideoTransport: () => VideoTransport,
|
|
29
|
+
useVideoEditor: () => useVideoEditor,
|
|
30
|
+
useVideoEditorController: () => useVideoEditorController,
|
|
31
|
+
useVideoEditorState: () => useVideoEditorState,
|
|
32
|
+
useVideoEditorTools: () => useVideoEditorTools
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(video_editor_exports);
|
|
35
|
+
|
|
36
|
+
// src/video-editor/useVideoEditor.ts
|
|
37
|
+
var import_react = require("react");
|
|
38
|
+
var import_video_editor = require("@pixerate/editor/video-editor");
|
|
39
|
+
function useVideoEditorController(options = {}) {
|
|
40
|
+
const ref = (0, import_react.useRef)(null);
|
|
41
|
+
if (!ref.current) ref.current = new import_video_editor.VideoEditorController(options);
|
|
42
|
+
const controller = ref.current;
|
|
43
|
+
(0, import_react.useEffect)(() => () => controller.destroy(), [controller]);
|
|
44
|
+
return controller;
|
|
45
|
+
}
|
|
46
|
+
function useVideoEditorState(controller) {
|
|
47
|
+
return (0, import_react.useSyncExternalStore)(controller.subscribe, controller.getState, controller.getState);
|
|
48
|
+
}
|
|
49
|
+
function useVideoEditor(controllerOrOptions = {}) {
|
|
50
|
+
const own = (0, import_react.useRef)(null);
|
|
51
|
+
let controller;
|
|
52
|
+
if (controllerOrOptions instanceof import_video_editor.VideoEditorController) controller = controllerOrOptions;
|
|
53
|
+
else {
|
|
54
|
+
if (!own.current) own.current = new import_video_editor.VideoEditorController(controllerOrOptions);
|
|
55
|
+
controller = own.current;
|
|
56
|
+
}
|
|
57
|
+
(0, import_react.useEffect)(() => () => own.current?.destroy(), []);
|
|
58
|
+
const state = useVideoEditorState(controller);
|
|
59
|
+
const actions = (0, import_react.useMemo)(
|
|
60
|
+
() => ({
|
|
61
|
+
play: () => controller.play(),
|
|
62
|
+
pause: () => controller.pause(),
|
|
63
|
+
toggle: () => controller.toggle(),
|
|
64
|
+
seek: (time) => controller.seek(time),
|
|
65
|
+
step: (frames) => controller.step(frames),
|
|
66
|
+
undo: () => void controller.undo(),
|
|
67
|
+
redo: () => void controller.redo(),
|
|
68
|
+
splitAtPlayhead: () => controller.splitAtPlayhead(),
|
|
69
|
+
deleteSelection: (options) => controller.deleteSelection(options)
|
|
70
|
+
}),
|
|
71
|
+
[controller]
|
|
72
|
+
);
|
|
73
|
+
return { controller, state, ...actions };
|
|
74
|
+
}
|
|
75
|
+
function useVideoEditorTools(registry, controller, options = {}) {
|
|
76
|
+
const { id, namespace, readOnly, rasterize } = options;
|
|
77
|
+
(0, import_react.useEffect)(() => {
|
|
78
|
+
if (!registry) return;
|
|
79
|
+
return registry.register((0, import_video_editor.createVideoEditorToolProvider)(controller, { id, namespace, readOnly, rasterize }));
|
|
80
|
+
}, [registry, controller, id, namespace, readOnly, rasterize]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/video-editor/VideoPreview.tsx
|
|
84
|
+
var import_react2 = require("react");
|
|
85
|
+
var import_video_editor2 = require("@pixerate/editor/video-editor");
|
|
86
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
87
|
+
function VideoPreview({ controller, resolution, onFrame, className = "", style }) {
|
|
88
|
+
const { project } = useVideoEditorState(controller);
|
|
89
|
+
const canvas = (0, import_react2.useRef)(null);
|
|
90
|
+
const frameRef = (0, import_react2.useRef)(onFrame);
|
|
91
|
+
frameRef.current = onFrame;
|
|
92
|
+
(0, import_react2.useEffect)(() => {
|
|
93
|
+
const el = canvas.current;
|
|
94
|
+
if (!el) return;
|
|
95
|
+
const engine = new import_video_editor2.VideoPreviewEngine(controller, el, { resolution, onFrame: (frame) => frameRef.current?.(frame) });
|
|
96
|
+
return () => engine.destroy();
|
|
97
|
+
}, [controller, resolution]);
|
|
98
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
99
|
+
"canvas",
|
|
100
|
+
{
|
|
101
|
+
ref: canvas,
|
|
102
|
+
className,
|
|
103
|
+
style: { display: "block", width: "100%", aspectRatio: `${project.width} / ${project.height}`, background: project.background ?? "#000", ...style },
|
|
104
|
+
role: "img",
|
|
105
|
+
"aria-label": `Preview of ${project.name ?? "the video"}`,
|
|
106
|
+
"data-video-preview": ""
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/video-editor/VideoTransport.tsx
|
|
112
|
+
var import_video_editor3 = require("@pixerate/editor/video-editor");
|
|
113
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
114
|
+
var btn = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
|
|
115
|
+
function VideoTransport({ controller, editable = true, className = "" }) {
|
|
116
|
+
const state = useVideoEditorState(controller);
|
|
117
|
+
const { time, duration, playing, project, selection, readOnly } = state;
|
|
118
|
+
const canEdit = editable && !readOnly;
|
|
119
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `flex flex-wrap items-center gap-1.5 text-xs ${className}`, "data-video-transport": "", children: [
|
|
120
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: btn, onClick: () => controller.step(-1), "aria-label": "Previous frame", title: "Previous frame (\u2190)", "data-step-back": "", children: "\u25C0|" }),
|
|
121
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.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" }),
|
|
122
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: btn, onClick: () => controller.step(1), "aria-label": "Next frame", title: "Next frame (\u2192)", "data-step-forward": "", children: "|\u25B6" }),
|
|
123
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "px-1 tabular-nums text-muted-foreground", "data-timecode": "", children: [
|
|
124
|
+
(0, import_video_editor3.formatVideoTimecode)(time, project.fps),
|
|
125
|
+
" / ",
|
|
126
|
+
(0, import_video_editor3.formatVideoTimecode)(duration, project.fps)
|
|
127
|
+
] }),
|
|
128
|
+
editable && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
129
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "mx-1 h-4 w-px bg-border" }),
|
|
130
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: btn, onClick: () => controller.splitAtPlayhead(), disabled: !canEdit, title: "Split at the playhead (S)", "data-split": "", children: "Split" }),
|
|
131
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: btn, onClick: () => controller.deleteSelection(), disabled: !canEdit || !selection.length, title: "Delete the selected clips (Delete)", "data-delete": "", children: "Delete" }),
|
|
132
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
133
|
+
"button",
|
|
134
|
+
{
|
|
135
|
+
type: "button",
|
|
136
|
+
className: btn,
|
|
137
|
+
onClick: () => controller.deleteSelection({ ripple: true }),
|
|
138
|
+
disabled: !canEdit || !selection.length,
|
|
139
|
+
title: "Delete the selected clips and close the gaps (Shift+Delete)",
|
|
140
|
+
"data-ripple-delete": "",
|
|
141
|
+
children: "Ripple delete"
|
|
142
|
+
}
|
|
143
|
+
),
|
|
144
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.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" }),
|
|
145
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.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" })
|
|
146
|
+
] })
|
|
147
|
+
] });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/video-editor/VideoTimeline.tsx
|
|
151
|
+
var import_react3 = require("react");
|
|
152
|
+
var import_video_editor4 = require("@pixerate/editor/video-editor");
|
|
153
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
154
|
+
var HEADER = 150;
|
|
155
|
+
var RULER = 24;
|
|
156
|
+
var ROW = 44;
|
|
157
|
+
var HANDLE = 6;
|
|
158
|
+
var DRAG_THRESHOLD = 3;
|
|
159
|
+
var MIN_ZOOM = 10;
|
|
160
|
+
var MAX_ZOOM = 400;
|
|
161
|
+
var isField = (el) => el instanceof HTMLElement && /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName);
|
|
162
|
+
function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable = true, className = "" }) {
|
|
163
|
+
const state = useVideoEditorState(controller);
|
|
164
|
+
const { project, duration, time, selection, readOnly, error } = state;
|
|
165
|
+
const [ownZoom, setOwnZoom] = (0, import_react3.useState)(pxPerSecond ?? 100);
|
|
166
|
+
const px = onPxPerSecondChange ? pxPerSecond ?? 100 : ownZoom;
|
|
167
|
+
const canEdit = editable && !readOnly;
|
|
168
|
+
const [drag, setDrag] = (0, import_react3.useState)(null);
|
|
169
|
+
const dragRef = (0, import_react3.useRef)(null);
|
|
170
|
+
const rows = (0, import_react3.useRef)(/* @__PURE__ */ new Map());
|
|
171
|
+
const ruler = (0, import_react3.useRef)(null);
|
|
172
|
+
const assets = (0, import_video_editor4.videoAssetMap)(project);
|
|
173
|
+
const msToPx = (ms) => ms * px / 1e3;
|
|
174
|
+
const span = Math.max(duration + 5e3, 1e4);
|
|
175
|
+
const width = msToPx(span);
|
|
176
|
+
const ticks = (0, import_video_editor4.videoRulerTicks)(span, px, project.fps);
|
|
177
|
+
const seekAt = (clientX) => {
|
|
178
|
+
const box = ruler.current.getBoundingClientRect();
|
|
179
|
+
controller.seek(Math.max(0, (clientX - box.left) * 1e3 / px));
|
|
180
|
+
};
|
|
181
|
+
const trackAt = (clipId, clientY) => {
|
|
182
|
+
const allowed = (0, import_video_editor4.videoDropTracks)(project, clipId);
|
|
183
|
+
for (const [id, el] of rows.current) {
|
|
184
|
+
const box = el.getBoundingClientRect();
|
|
185
|
+
if (clientY >= box.top && clientY < box.bottom && allowed.includes(id)) return id;
|
|
186
|
+
}
|
|
187
|
+
return void 0;
|
|
188
|
+
};
|
|
189
|
+
const planFor = (d, e) => (0, import_video_editor4.planVideoClipDrag)(project, {
|
|
190
|
+
clip: d.clip,
|
|
191
|
+
mode: d.mode,
|
|
192
|
+
delta: (e.clientX - d.x) * 1e3 / px,
|
|
193
|
+
track: d.mode === "move" ? trackAt(d.clip, e.clientY) : void 0,
|
|
194
|
+
ripple: e.altKey,
|
|
195
|
+
snap: (t) => controller.snap(t, { exclude: [d.clip] }).time
|
|
196
|
+
});
|
|
197
|
+
const update = (next) => {
|
|
198
|
+
dragRef.current = next;
|
|
199
|
+
setDrag(next);
|
|
200
|
+
};
|
|
201
|
+
const live = (0, import_react3.useRef)({ planFor, selection });
|
|
202
|
+
live.current = { planFor, selection };
|
|
203
|
+
const stopDrag = (0, import_react3.useRef)(null);
|
|
204
|
+
(0, import_react3.useEffect)(() => () => stopDrag.current?.(), []);
|
|
205
|
+
const onClipPointerDown = (e, clip, track) => {
|
|
206
|
+
if (e.button !== void 0 && e.button !== 0) return;
|
|
207
|
+
e.stopPropagation();
|
|
208
|
+
if (!editable) return;
|
|
209
|
+
try {
|
|
210
|
+
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
211
|
+
} catch {
|
|
212
|
+
}
|
|
213
|
+
const handle = e.target.closest("[data-handle]")?.getAttribute("data-handle");
|
|
214
|
+
const mode = handle === "start" ? "trim-start" : handle === "end" ? "trim-end" : "move";
|
|
215
|
+
const fixed = !canEdit || !!track.locked;
|
|
216
|
+
stopDrag.current?.();
|
|
217
|
+
update({ clip: clip.id, mode, x: e.clientX, pointer: e.pointerId, moved: false, plan: null });
|
|
218
|
+
const move = (ev) => {
|
|
219
|
+
const d = dragRef.current;
|
|
220
|
+
if (!d || fixed) return;
|
|
221
|
+
if (!d.moved && Math.abs(ev.clientX - d.x) < DRAG_THRESHOLD) return;
|
|
222
|
+
if (!d.moved && !live.current.selection.includes(d.clip)) controller.select([d.clip]);
|
|
223
|
+
update({ ...d, moved: true, plan: live.current.planFor(d, ev) });
|
|
224
|
+
};
|
|
225
|
+
const up = (ev) => {
|
|
226
|
+
stop();
|
|
227
|
+
const d = dragRef.current;
|
|
228
|
+
if (!d) return;
|
|
229
|
+
update(null);
|
|
230
|
+
if (!d.moved) {
|
|
231
|
+
if (ev.shiftKey || ev.metaKey || ev.ctrlKey) controller.select([d.clip], { add: true });
|
|
232
|
+
else controller.select([d.clip]);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const plan = live.current.planFor(d, ev);
|
|
236
|
+
if (plan?.edit) controller.apply(plan.edit, d.mode === "move" ? "Move clip" : ev.altKey ? "Ripple trim" : "Trim clip");
|
|
237
|
+
};
|
|
238
|
+
const cancel = () => {
|
|
239
|
+
stop();
|
|
240
|
+
update(null);
|
|
241
|
+
};
|
|
242
|
+
const stop = () => {
|
|
243
|
+
window.removeEventListener("pointermove", move);
|
|
244
|
+
window.removeEventListener("pointerup", up);
|
|
245
|
+
window.removeEventListener("pointercancel", cancel);
|
|
246
|
+
stopDrag.current = null;
|
|
247
|
+
};
|
|
248
|
+
window.addEventListener("pointermove", move);
|
|
249
|
+
window.addEventListener("pointerup", up);
|
|
250
|
+
window.addEventListener("pointercancel", cancel);
|
|
251
|
+
stopDrag.current = stop;
|
|
252
|
+
};
|
|
253
|
+
const onKeyDown = (e) => {
|
|
254
|
+
if (isField(e.target)) return;
|
|
255
|
+
const mod = e.metaKey || e.ctrlKey;
|
|
256
|
+
const key = e.key.toLowerCase();
|
|
257
|
+
if (e.key === " ") {
|
|
258
|
+
if (e.target.tagName === "BUTTON") return;
|
|
259
|
+
controller.toggle();
|
|
260
|
+
} else if (e.key === "ArrowLeft" && !mod) controller.step(-1);
|
|
261
|
+
else if (e.key === "ArrowRight" && !mod) controller.step(1);
|
|
262
|
+
else if (e.key === "Escape") controller.select(null);
|
|
263
|
+
else if (!canEdit) return;
|
|
264
|
+
else if (key === "s" && !mod) controller.splitAtPlayhead();
|
|
265
|
+
else if ((e.key === "Delete" || e.key === "Backspace") && !mod) controller.deleteSelection({ ripple: e.shiftKey });
|
|
266
|
+
else if (key === "z" && mod) {
|
|
267
|
+
if (e.shiftKey) controller.redo();
|
|
268
|
+
else controller.undo();
|
|
269
|
+
} else return;
|
|
270
|
+
e.preventDefault();
|
|
271
|
+
};
|
|
272
|
+
const placed = project.tracks.map((track) => ({
|
|
273
|
+
track,
|
|
274
|
+
clips: project.tracks.flatMap(
|
|
275
|
+
(t) => t.clips.map((clip) => {
|
|
276
|
+
const plan = drag?.moved && drag.clip === clip.id ? drag.plan : null;
|
|
277
|
+
const on = plan ? plan.track : t.id;
|
|
278
|
+
return on === track.id ? { clip, home: t, start: plan ? plan.start : clip.start, duration: plan ? plan.duration : clip.duration, dragging: !!plan } : null;
|
|
279
|
+
}).filter((x) => x !== null)
|
|
280
|
+
)
|
|
281
|
+
}));
|
|
282
|
+
const toggle = (track, field2, label) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
283
|
+
"button",
|
|
284
|
+
{
|
|
285
|
+
type: "button",
|
|
286
|
+
className: `rounded border px-1 text-[10px] leading-4 ${track[field2] ? "border-rose-500 bg-rose-500/15 text-rose-600" : "border-border text-muted-foreground"} disabled:opacity-40`,
|
|
287
|
+
"aria-pressed": !!track[field2],
|
|
288
|
+
"aria-label": `${label} ${track.name ?? track.id}`,
|
|
289
|
+
title: label,
|
|
290
|
+
disabled: !canEdit,
|
|
291
|
+
"data-track-toggle": field2,
|
|
292
|
+
onClick: () => controller.apply({ op: "update_track", track: track.id, set: { [field2]: !track[field2] } }, `${track[field2] ? "Un" : ""}${field2 === "muted" ? "mute" : field2 === "hidden" ? "hide" : "lock"} track`),
|
|
293
|
+
children: field2 === "muted" ? "M" : field2 === "hidden" ? "H" : "L"
|
|
294
|
+
}
|
|
295
|
+
);
|
|
296
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `flex flex-col gap-1 ${className}`, "data-video-timeline-wrapper": "", children: [
|
|
297
|
+
!onPxPerSecondChange && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "flex items-center gap-2 self-end text-xs text-muted-foreground", children: [
|
|
298
|
+
"Zoom",
|
|
299
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.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": "" })
|
|
300
|
+
] }),
|
|
301
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
302
|
+
"div",
|
|
303
|
+
{
|
|
304
|
+
className: "relative overflow-x-auto rounded border border-border outline-none focus-visible:ring-2 focus-visible:ring-rose-500/50",
|
|
305
|
+
tabIndex: 0,
|
|
306
|
+
role: "group",
|
|
307
|
+
"aria-label": "Video timeline",
|
|
308
|
+
onKeyDown,
|
|
309
|
+
"data-video-timeline": "",
|
|
310
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "relative", style: { width: HEADER + width }, children: [
|
|
311
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex", style: { height: RULER }, children: [
|
|
312
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "sticky left-0 z-20 shrink-0 border-b border-r border-border bg-background", style: { width: HEADER } }),
|
|
313
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
314
|
+
"div",
|
|
315
|
+
{
|
|
316
|
+
ref: ruler,
|
|
317
|
+
className: "relative shrink-0 cursor-ew-resize select-none touch-none border-b border-border text-[10px] text-muted-foreground",
|
|
318
|
+
style: { width },
|
|
319
|
+
role: "slider",
|
|
320
|
+
"aria-label": "Playhead",
|
|
321
|
+
"aria-valuemin": 0,
|
|
322
|
+
"aria-valuemax": duration,
|
|
323
|
+
"aria-valuenow": Math.round(time),
|
|
324
|
+
"data-ruler": "",
|
|
325
|
+
onPointerDown: (e) => {
|
|
326
|
+
try {
|
|
327
|
+
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
328
|
+
} catch {
|
|
329
|
+
}
|
|
330
|
+
seekAt(e.clientX);
|
|
331
|
+
},
|
|
332
|
+
onPointerMove: (e) => {
|
|
333
|
+
if (e.buttons & 1) seekAt(e.clientX);
|
|
334
|
+
},
|
|
335
|
+
children: [
|
|
336
|
+
ticks.map((t) => /* @__PURE__ */ (0, import_jsx_runtime3.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_runtime3.jsx)("span", { className: "absolute -top-3.5 left-1 whitespace-nowrap tabular-nums", children: t.label }) }, t.time)),
|
|
337
|
+
(project.markers ?? []).map((m) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
338
|
+
"div",
|
|
339
|
+
{
|
|
340
|
+
className: "absolute top-0 h-2.5 w-2.5 -translate-x-1/2 rotate-45",
|
|
341
|
+
style: { left: msToPx(m.time), background: m.color ?? "#f59e0b" },
|
|
342
|
+
title: `${m.label ?? m.id} at ${(0, import_video_editor4.formatVideoSeconds)(m.time)}`,
|
|
343
|
+
"data-marker": m.id
|
|
344
|
+
},
|
|
345
|
+
m.id
|
|
346
|
+
))
|
|
347
|
+
]
|
|
348
|
+
}
|
|
349
|
+
)
|
|
350
|
+
] }),
|
|
351
|
+
placed.map(({ track, clips }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex border-b border-border last:border-b-0", style: { height: ROW }, "data-track": track.id, children: [
|
|
352
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.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: [
|
|
353
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-1", children: [
|
|
354
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "truncate font-medium", children: track.name ?? track.id }),
|
|
355
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "rounded bg-muted px-1 text-[10px] uppercase text-muted-foreground", "data-track-kind": "", children: track.kind })
|
|
356
|
+
] }),
|
|
357
|
+
editable && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex gap-1", children: [
|
|
358
|
+
toggle(track, "muted", "Mute"),
|
|
359
|
+
track.kind === "video" && toggle(track, "hidden", "Hide"),
|
|
360
|
+
toggle(track, "locked", "Lock")
|
|
361
|
+
] })
|
|
362
|
+
] }),
|
|
363
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
364
|
+
"div",
|
|
365
|
+
{
|
|
366
|
+
ref: (el) => {
|
|
367
|
+
if (el) rows.current.set(track.id, el);
|
|
368
|
+
else rows.current.delete(track.id);
|
|
369
|
+
},
|
|
370
|
+
className: `relative shrink-0 ${track.hidden ? "opacity-50" : ""} ${track.locked ? "bg-muted/40" : ""}`,
|
|
371
|
+
style: { width },
|
|
372
|
+
"data-track-lane": track.id,
|
|
373
|
+
onPointerDown: (e) => {
|
|
374
|
+
if (e.target === e.currentTarget && editable) controller.select(null);
|
|
375
|
+
},
|
|
376
|
+
children: clips.map(({ clip, home, start, duration: length, dragging }) => {
|
|
377
|
+
const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
|
|
378
|
+
const selected = selection.includes(clip.id);
|
|
379
|
+
const label = (0, import_video_editor4.videoClipLabel)(clip, asset);
|
|
380
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
381
|
+
"div",
|
|
382
|
+
{
|
|
383
|
+
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" : ""}`,
|
|
384
|
+
style: {
|
|
385
|
+
left: msToPx(start),
|
|
386
|
+
width: Math.max(2, msToPx(length)),
|
|
387
|
+
background: (0, import_video_editor4.videoClipTint)(clip, asset),
|
|
388
|
+
outline: selected ? "2px solid #f43f5e" : void 0,
|
|
389
|
+
outlineOffset: selected ? 1 : void 0,
|
|
390
|
+
zIndex: dragging ? 10 : selected ? 2 : 1
|
|
391
|
+
},
|
|
392
|
+
title: `${label} \xB7 ${(0, import_video_editor4.formatVideoSeconds)(start)}\u2013${(0, import_video_editor4.formatVideoSeconds)(start + length)}`,
|
|
393
|
+
"data-clip": clip.id,
|
|
394
|
+
"data-selected": selected ? "" : void 0,
|
|
395
|
+
"data-dragging": dragging ? "" : void 0,
|
|
396
|
+
"aria-selected": selected,
|
|
397
|
+
onPointerDown: (e) => onClipPointerDown(e, clip, home),
|
|
398
|
+
children: [
|
|
399
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "pointer-events-none block truncate px-1.5 py-0.5 leading-4", children: label }),
|
|
400
|
+
clip.transition && /* @__PURE__ */ (0, import_jsx_runtime3.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: [
|
|
401
|
+
"\u29D3 ",
|
|
402
|
+
clip.transition.type
|
|
403
|
+
] }),
|
|
404
|
+
canEdit && !home.locked && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
405
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.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" }),
|
|
406
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.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" })
|
|
407
|
+
] })
|
|
408
|
+
]
|
|
409
|
+
},
|
|
410
|
+
clip.id
|
|
411
|
+
);
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
)
|
|
415
|
+
] }, track.id)),
|
|
416
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.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": "" })
|
|
417
|
+
] })
|
|
418
|
+
}
|
|
419
|
+
),
|
|
420
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-xs text-red-500", role: "alert", "data-edit-error": "", children: error })
|
|
421
|
+
] });
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// src/video-editor/ClipInspector.tsx
|
|
425
|
+
var import_video_editor5 = require("@pixerate/editor/video-editor");
|
|
426
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
427
|
+
var field = "w-full rounded border border-border bg-transparent px-1 py-0.5 text-xs disabled:opacity-50";
|
|
428
|
+
var DEFAULT_TRANSITION = 500;
|
|
429
|
+
function Commit({
|
|
430
|
+
value,
|
|
431
|
+
onCommit,
|
|
432
|
+
multiline,
|
|
433
|
+
...rest
|
|
434
|
+
}) {
|
|
435
|
+
const commit = (raw) => {
|
|
436
|
+
if (raw !== value) onCommit(raw);
|
|
437
|
+
};
|
|
438
|
+
if (multiline)
|
|
439
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
440
|
+
"textarea",
|
|
441
|
+
{
|
|
442
|
+
className: field,
|
|
443
|
+
rows: 2,
|
|
444
|
+
defaultValue: value,
|
|
445
|
+
disabled: rest.disabled,
|
|
446
|
+
"aria-label": rest["aria-label"],
|
|
447
|
+
onBlur: (e) => commit(e.currentTarget.value)
|
|
448
|
+
},
|
|
449
|
+
value
|
|
450
|
+
);
|
|
451
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("input", { className: field, ...rest, defaultValue: value, onKeyDown: (e) => e.key === "Enter" && e.currentTarget.blur(), onBlur: (e) => commit(e.currentTarget.value) }, value);
|
|
452
|
+
}
|
|
453
|
+
function Row({ label, name, children }) {
|
|
454
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "grid grid-cols-[5.5rem_minmax(0,1fr)] items-center gap-2 text-xs", "data-field": name, children: [
|
|
455
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-muted-foreground", children: label }),
|
|
456
|
+
children
|
|
457
|
+
] });
|
|
458
|
+
}
|
|
459
|
+
function ClipInspector({ controller, className = "" }) {
|
|
460
|
+
const state = useVideoEditorState(controller);
|
|
461
|
+
const { project, selection, readOnly } = state;
|
|
462
|
+
if (selection.length !== 1)
|
|
463
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.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." });
|
|
464
|
+
const at = (0, import_video_editor5.findVideoClip)(project, selection[0]);
|
|
465
|
+
if (!at) return null;
|
|
466
|
+
const clip = at.clip;
|
|
467
|
+
const disabled = readOnly || !!at.track.locked;
|
|
468
|
+
const set = (key, value, label = `Change ${key}`) => {
|
|
469
|
+
if (value === void 0 || value === "") controller.apply({ op: "update_clip", clip: clip.id, remove: [key] }, label);
|
|
470
|
+
else controller.apply({ op: "update_clip", clip: clip.id, set: { [key]: value } }, label);
|
|
471
|
+
};
|
|
472
|
+
const num = (key, raw, label) => {
|
|
473
|
+
const trimmed = raw.trim();
|
|
474
|
+
if (!trimmed) return set(key, void 0, label);
|
|
475
|
+
const n = Number(trimmed);
|
|
476
|
+
if (Number.isFinite(n)) set(key, n, label);
|
|
477
|
+
};
|
|
478
|
+
const text = (key, label) => {
|
|
479
|
+
const value = clip[key];
|
|
480
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Commit, { value: value === void 0 ? "" : String(value), "aria-label": label ?? key, disabled, onCommit: (raw) => set(key, raw.trim() || void 0, `Change ${label ?? key}`) });
|
|
481
|
+
};
|
|
482
|
+
const number = (key, label, attrs = {}) => {
|
|
483
|
+
const value = clip[key];
|
|
484
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Commit, { type: "number", value: value === void 0 ? "" : String(value), "aria-label": label, disabled, onCommit: (raw) => num(key, raw, `Change ${label.toLowerCase()}`), ...attrs });
|
|
485
|
+
};
|
|
486
|
+
const transition = clip.transition;
|
|
487
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: `flex flex-col gap-1.5 ${className}`, "data-clip-inspector": "", "data-clip-id": clip.id, children: [
|
|
488
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center gap-2 text-xs", children: [
|
|
489
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "font-medium", children: clip.type === "media" ? "Media clip" : clip.type === "text" ? "Title" : "Colour clip" }),
|
|
490
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("code", { className: "text-muted-foreground", children: clip.id }),
|
|
491
|
+
at.track.locked && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-amber-500", children: "locked track" })
|
|
492
|
+
] }),
|
|
493
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Name", name: "name", children: text("name", "Name") }),
|
|
494
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Start", name: "start", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "tabular-nums text-xs", children: (0, import_video_editor5.formatVideoSeconds)(clip.start) }) }),
|
|
495
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Duration", name: "duration", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "tabular-nums text-xs", children: (0, import_video_editor5.formatVideoSeconds)(clip.duration) }) }),
|
|
496
|
+
clip.type !== "media" || project.assets.find((a) => a.id === clip.asset)?.kind !== "audio" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Opacity", name: "opacity", children: number("opacity", "Opacity", { min: 0, max: 1, step: 0.05 }) }) : null,
|
|
497
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Fade in (ms)", name: "fadeIn", children: number("fadeIn", "Fade in", { min: 0, step: 100 }) }),
|
|
498
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Fade out (ms)", name: "fadeOut", children: number("fadeOut", "Fade out", { min: 0, step: 100 }) }),
|
|
499
|
+
clip.type === "media" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
500
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Volume", name: "volume", children: number("volume", "Volume", { min: 0, step: 0.1 }) }),
|
|
501
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Speed", name: "speed", children: number("speed", "Speed", { min: 0.1, step: 0.25 }) }),
|
|
502
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Muted", name: "muted", children: /* @__PURE__ */ (0, import_jsx_runtime4.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") }) })
|
|
503
|
+
] }),
|
|
504
|
+
clip.type === "text" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
505
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Text", name: "text", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Commit, { multiline: true, value: clip.text, "aria-label": "Text", disabled, onCommit: (raw) => set("text", raw, "Change text") }) }),
|
|
506
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Colour", name: "color", children: text("color", "Colour") }),
|
|
507
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Font size", name: "fontSize", children: number("fontSize", "Font size", { min: 1, step: 2 }) }),
|
|
508
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Background", name: "background", children: text("background", "Background") })
|
|
509
|
+
] }),
|
|
510
|
+
clip.type === "color" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Colour", name: "color", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Commit, { value: clip.color, "aria-label": "Colour", disabled, onCommit: (raw) => raw.trim() && set("color", raw.trim(), "Change colour") }) }),
|
|
511
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Transition in", name: "transition", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
512
|
+
"select",
|
|
513
|
+
{
|
|
514
|
+
className: field,
|
|
515
|
+
value: transition?.type ?? "",
|
|
516
|
+
disabled,
|
|
517
|
+
"aria-label": "Transition",
|
|
518
|
+
onChange: (e) => {
|
|
519
|
+
const type = e.target.value;
|
|
520
|
+
controller.apply(
|
|
521
|
+
{ op: "set_transition", clip: clip.id, transition: type ? { ...transition, type, duration: transition?.duration ?? DEFAULT_TRANSITION } : null },
|
|
522
|
+
type ? "Set transition" : "Remove transition"
|
|
523
|
+
);
|
|
524
|
+
},
|
|
525
|
+
children: [
|
|
526
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "", children: "None" }),
|
|
527
|
+
import_video_editor5.VIDEO_TRANSITION_TYPES.map((t) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: t, children: t === "crossfade" ? "Crossfade" : t === "dip" ? "Dip to colour" : "Wipe" }, t))
|
|
528
|
+
]
|
|
529
|
+
}
|
|
530
|
+
) }),
|
|
531
|
+
transition && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Length (ms)", name: "transitionDuration", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
532
|
+
Commit,
|
|
533
|
+
{
|
|
534
|
+
type: "number",
|
|
535
|
+
min: 1,
|
|
536
|
+
step: 100,
|
|
537
|
+
value: String(transition.duration),
|
|
538
|
+
"aria-label": "Transition length",
|
|
539
|
+
disabled,
|
|
540
|
+
onCommit: (raw) => {
|
|
541
|
+
const n = Number(raw);
|
|
542
|
+
if (raw.trim() && Number.isFinite(n) && n > 0) controller.apply({ op: "set_transition", clip: clip.id, transition: { ...transition, duration: n } }, "Change transition length");
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
) })
|
|
546
|
+
] });
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// src/video-editor/MediaBin.tsx
|
|
550
|
+
var import_react4 = require("react");
|
|
551
|
+
var import_video_editor6 = require("@pixerate/editor/video-editor");
|
|
552
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
553
|
+
var btn2 = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
|
|
554
|
+
function MediaBin({ controller, editable = true, fileToUrl, probe = import_video_editor6.probeVideoMedia, className = "" }) {
|
|
555
|
+
const { project, readOnly } = useVideoEditorState(controller);
|
|
556
|
+
const canEdit = editable && !readOnly;
|
|
557
|
+
const [url, setUrl] = (0, import_react4.useState)("");
|
|
558
|
+
const [urlKind, setUrlKind] = (0, import_react4.useState)("");
|
|
559
|
+
const [busy, setBusy] = (0, import_react4.useState)(0);
|
|
560
|
+
const [problem, setProblem] = (0, import_react4.useState)(null);
|
|
561
|
+
const [over, setOver] = (0, import_react4.useState)(false);
|
|
562
|
+
const addSource = async (src, kind, name) => {
|
|
563
|
+
setBusy((n) => n + 1);
|
|
564
|
+
try {
|
|
565
|
+
const found = await probe(src, kind).catch(() => null);
|
|
566
|
+
const result = controller.apply({ op: "add_asset", asset: { kind, src, name, ...found ?? {} } }, `Import ${name}`);
|
|
567
|
+
if (!result.ok) setProblem(result.error);
|
|
568
|
+
} finally {
|
|
569
|
+
setBusy((n) => n - 1);
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
const addFiles = async (files) => {
|
|
573
|
+
setProblem(null);
|
|
574
|
+
for (const file of Array.from(files ?? [])) {
|
|
575
|
+
const kind = (0, import_video_editor6.videoAssetKindOf)(file.type || file.name);
|
|
576
|
+
if (!kind) {
|
|
577
|
+
setProblem(`"${file.name}" is not a video, audio or image file.`);
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
const src = fileToUrl ? fileToUrl(file) : URL.createObjectURL(file);
|
|
581
|
+
await addSource(src, kind, file.name);
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
const addUrl = async () => {
|
|
585
|
+
const src = url.trim();
|
|
586
|
+
if (!src) return;
|
|
587
|
+
const kind = urlKind || (0, import_video_editor6.videoAssetKindOf)(src.replace(/[?#].*$/, ""));
|
|
588
|
+
if (!kind) {
|
|
589
|
+
setProblem(`Can't tell whether "${src}" is video, audio or an image; choose its kind.`);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
setProblem(null);
|
|
593
|
+
await addSource(src, kind, src.replace(/[?#].*$/, "").replace(/^.*[\\/]/, "") || src);
|
|
594
|
+
setUrl("");
|
|
595
|
+
};
|
|
596
|
+
const addToTimeline = (asset) => {
|
|
597
|
+
const edit = (0, import_video_editor6.videoAssetDropEdit)(project, asset.id);
|
|
598
|
+
if (!edit) {
|
|
599
|
+
setProblem(`There is no unlocked ${asset.kind === "audio" ? "audio" : "video"} track to put "${asset.name ?? asset.id}" on.`);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
setProblem(null);
|
|
603
|
+
const result = controller.apply(edit, `Add ${asset.name ?? asset.id}`);
|
|
604
|
+
if (result.ok) {
|
|
605
|
+
const added = result.outcomes[0]?.clip;
|
|
606
|
+
if (added) controller.select([added]);
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
610
|
+
"div",
|
|
611
|
+
{
|
|
612
|
+
className: `flex flex-col gap-2 rounded ${over ? "ring-2 ring-rose-500/60" : ""} ${className}`,
|
|
613
|
+
"data-media-bin": "",
|
|
614
|
+
onDragOver: (e) => {
|
|
615
|
+
if (!canEdit || !e.dataTransfer?.types?.includes("Files")) return;
|
|
616
|
+
e.preventDefault();
|
|
617
|
+
setOver(true);
|
|
618
|
+
},
|
|
619
|
+
onDragLeave: () => setOver(false),
|
|
620
|
+
onDrop: (e) => {
|
|
621
|
+
setOver(false);
|
|
622
|
+
if (!canEdit || !e.dataTransfer?.files?.length) return;
|
|
623
|
+
e.preventDefault();
|
|
624
|
+
void addFiles(e.dataTransfer.files);
|
|
625
|
+
},
|
|
626
|
+
children: [
|
|
627
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "text-xs font-medium", children: "Media" }),
|
|
628
|
+
project.assets.length ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("ul", { className: "flex flex-col gap-1", "aria-label": "Media assets", children: project.assets.map((asset) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("li", { className: "flex items-center gap-2 text-xs", "data-asset": asset.id, children: [
|
|
629
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "rounded bg-muted px-1 text-[10px] uppercase text-muted-foreground", "data-asset-kind": "", children: asset.kind }),
|
|
630
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "min-w-0 flex-1 truncate", title: asset.src, children: asset.name ?? asset.id }),
|
|
631
|
+
asset.duration !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "tabular-nums text-muted-foreground", children: (0, import_video_editor6.formatVideoSeconds)(asset.duration) }),
|
|
632
|
+
editable && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: btn2, disabled: !canEdit, onClick: () => addToTimeline(asset), "data-add-to-timeline": "", children: "Add to timeline" })
|
|
633
|
+
] }, asset.id)) }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { className: "text-xs text-muted-foreground", children: [
|
|
634
|
+
"No media yet.",
|
|
635
|
+
editable ? " Choose or drop files, or add a URL." : ""
|
|
636
|
+
] }),
|
|
637
|
+
editable && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
|
|
638
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "flex flex-col gap-1 rounded border border-dashed border-border p-2 text-xs text-muted-foreground", children: [
|
|
639
|
+
"Choose or drop video, audio or image files",
|
|
640
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
641
|
+
"input",
|
|
642
|
+
{
|
|
643
|
+
type: "file",
|
|
644
|
+
multiple: true,
|
|
645
|
+
accept: "video/*,audio/*,image/*",
|
|
646
|
+
disabled: !canEdit,
|
|
647
|
+
"aria-label": "Import media files",
|
|
648
|
+
"data-import-files": "",
|
|
649
|
+
onChange: (e) => {
|
|
650
|
+
const files = e.currentTarget.files ? Array.from(e.currentTarget.files) : [];
|
|
651
|
+
e.currentTarget.value = "";
|
|
652
|
+
void addFiles(files);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
)
|
|
656
|
+
] }),
|
|
657
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
658
|
+
"form",
|
|
659
|
+
{
|
|
660
|
+
className: "flex items-center gap-1",
|
|
661
|
+
onSubmit: (e) => {
|
|
662
|
+
e.preventDefault();
|
|
663
|
+
void addUrl();
|
|
664
|
+
},
|
|
665
|
+
children: [
|
|
666
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
667
|
+
"input",
|
|
668
|
+
{
|
|
669
|
+
className: "min-w-0 flex-1 rounded border border-border bg-transparent px-1 py-0.5 text-xs",
|
|
670
|
+
placeholder: "https://\u2026/clip.mp4",
|
|
671
|
+
value: url,
|
|
672
|
+
disabled: !canEdit,
|
|
673
|
+
"aria-label": "Media URL",
|
|
674
|
+
onChange: (e) => setUrl(e.target.value)
|
|
675
|
+
}
|
|
676
|
+
),
|
|
677
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.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: [
|
|
678
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: "Auto" }),
|
|
679
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "video", children: "Video" }),
|
|
680
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "audio", children: "Audio" }),
|
|
681
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "image", children: "Image" })
|
|
682
|
+
] }),
|
|
683
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "submit", className: btn2, disabled: !canEdit || !url.trim(), "data-add-url": "", children: "Add" })
|
|
684
|
+
]
|
|
685
|
+
}
|
|
686
|
+
)
|
|
687
|
+
] }),
|
|
688
|
+
busy > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-xs text-muted-foreground", children: "Reading media\u2026" }),
|
|
689
|
+
problem && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-xs text-red-500", role: "alert", "data-media-error": "", children: problem })
|
|
690
|
+
]
|
|
691
|
+
}
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// src/video-editor/VideoEditor.tsx
|
|
696
|
+
var import_react5 = require("react");
|
|
697
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
698
|
+
function VideoEditor({ controller: given, options, editable = true, pxPerSecond, onPxPerSecondChange, resolution, showIssues = true, className = "" }) {
|
|
699
|
+
const { controller, state } = useVideoEditor(given ?? options ?? {});
|
|
700
|
+
(0, import_react5.useEffect)(() => {
|
|
701
|
+
if (editable) return;
|
|
702
|
+
const was = controller.getState().readOnly;
|
|
703
|
+
controller.setReadOnly(true);
|
|
704
|
+
return () => controller.setReadOnly(was);
|
|
705
|
+
}, [controller, editable]);
|
|
706
|
+
const { issues } = state;
|
|
707
|
+
const issueList = showIssues && issues.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.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_runtime6.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)) });
|
|
708
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: `flex flex-col gap-3 ${className}`, "data-video-editor": "", children: [
|
|
709
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: `grid gap-3 ${editable ? "lg:grid-cols-[16rem_minmax(0,1fr)_18rem]" : ""}`, children: [
|
|
710
|
+
editable && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MediaBin, { controller, className: "min-w-0" }),
|
|
711
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex min-w-0 flex-col gap-2", children: [
|
|
712
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(VideoPreview, { controller, resolution, className: "rounded-md border border-border" }),
|
|
713
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(VideoTransport, { controller, editable })
|
|
714
|
+
] }),
|
|
715
|
+
editable && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ClipInspector, { controller, className: "min-w-0" })
|
|
716
|
+
] }),
|
|
717
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(VideoTimeline, { controller, editable, pxPerSecond, onPxPerSecondChange }),
|
|
718
|
+
issueList
|
|
719
|
+
] });
|
|
720
|
+
}
|
|
721
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
722
|
+
0 && (module.exports = {
|
|
723
|
+
ClipInspector,
|
|
724
|
+
MediaBin,
|
|
725
|
+
VideoEditor,
|
|
726
|
+
VideoPreview,
|
|
727
|
+
VideoTimeline,
|
|
728
|
+
VideoTransport,
|
|
729
|
+
useVideoEditor,
|
|
730
|
+
useVideoEditorController,
|
|
731
|
+
useVideoEditorState,
|
|
732
|
+
useVideoEditorTools
|
|
733
|
+
});
|
|
734
|
+
//# sourceMappingURL=index.cjs.map
|