@cut-workbench/workbench 0.1.0 → 0.3.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.
- package/README.md +153 -9
- package/dist/CutWorkbench.d.ts +26 -1
- package/dist/auto.cjs +4 -0
- package/dist/auto.d.ts +19 -0
- package/dist/auto.js +8 -0
- package/dist/components/common/UnsupportedBrowser.d.ts +6 -1
- package/dist/components/layout/AppShell.d.ts +17 -3
- package/dist/components/layout/WorkbenchToolbar.d.ts +39 -2
- package/dist/cut-workbench.css +47 -6
- package/dist/features/lab/index.d.ts +5 -1
- package/dist/features/media-library/components/DropZone.d.ts +6 -0
- package/dist/features/media-library/lib/pick-files.d.ts +24 -0
- package/dist/hooks/use-toast-safe.d.ts +9 -1
- package/dist/index.cjs +937 -1969
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +825 -1836
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +3 -0
- package/dist/lab-disabled.cjs +6 -0
- package/dist/lab-disabled.cjs.map +1 -0
- package/dist/lab-disabled.d.ts +13 -0
- package/dist/lab-disabled.js +5 -0
- package/dist/lab-disabled.js.map +1 -0
- package/dist/lab.cjs +1194 -0
- package/dist/lab.cjs.map +1 -0
- package/dist/lab.d.ts +32 -0
- package/dist/lab.js +1193 -0
- package/dist/lab.js.map +1 -0
- package/dist/lib/notify.d.ts +40 -0
- package/dist/notify-CEwFQgjC.js +233 -0
- package/dist/notify-CEwFQgjC.js.map +1 -0
- package/dist/notify-CMNwDFGw.cjs +380 -0
- package/dist/notify-CMNwDFGw.cjs.map +1 -0
- package/dist/umd/cut-workbench.umd.js +1 -1
- package/package.json +84 -67
package/dist/index.cjs
CHANGED
|
@@ -1,90 +1,76 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
|
|
3
|
-
var __create = Object.create;
|
|
4
|
-
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
-
var __copyProps = (to, from, except, desc) => {
|
|
10
|
-
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
-
key = keys[i];
|
|
12
|
-
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
-
get: ((k) => from[k]).bind(null, key),
|
|
14
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
return to;
|
|
18
|
-
};
|
|
19
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
20
|
-
value: mod,
|
|
21
|
-
enumerable: true
|
|
22
|
-
}) : target, mod));
|
|
2
|
+
const require_notify = require("./notify-CMNwDFGw.cjs");
|
|
23
3
|
let react_dom_client = require("react-dom/client");
|
|
24
4
|
let react = require("react");
|
|
25
|
-
react = __toESM(react, 1);
|
|
5
|
+
react = require_notify.__toESM(react, 1);
|
|
26
6
|
let _cut_workbench_core_engine = require("@cut-workbench/core/engine");
|
|
27
7
|
let _cut_workbench_core = require("@cut-workbench/core");
|
|
28
8
|
let _cut_workbench_react = require("@cut-workbench/react");
|
|
29
|
-
let clsx = require("clsx");
|
|
30
|
-
let tailwind_merge = require("tailwind-merge");
|
|
31
9
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
32
10
|
let lucide_react = require("lucide-react");
|
|
33
11
|
let class_variance_authority = require("class-variance-authority");
|
|
34
12
|
let radix_ui = require("radix-ui");
|
|
35
13
|
let sonner = require("sonner");
|
|
36
14
|
let react_dropzone = require("react-dropzone");
|
|
37
|
-
let _cut_workbench_core_lab = require("@cut-workbench/core/lab");
|
|
38
15
|
let zustand_vanilla = require("zustand/vanilla");
|
|
39
|
-
// src/lib/cn.ts
|
|
40
|
-
/**
|
|
41
|
-
* 合并 className:tailwind-merge 负责消解冲突,clsx 负责条件拼接。
|
|
42
|
-
*/
|
|
43
|
-
function cn(...inputs) {
|
|
44
|
-
return (0, tailwind_merge.twMerge)((0, clsx.clsx)(inputs));
|
|
45
|
-
}
|
|
46
16
|
// src/components/layout/AppShell.tsx
|
|
47
17
|
/**
|
|
48
|
-
*
|
|
18
|
+
* 工作台骨架:顶栏 + 上半区(素材 / 预览 / 属性)+ 底部通栏时间轴。
|
|
19
|
+
*
|
|
20
|
+
* ```
|
|
21
|
+
* ┌───────────────────────────────────────────┐
|
|
22
|
+
* │ 顶栏 │
|
|
23
|
+
* ├──────────┬──────────────┬─────────────────┤
|
|
24
|
+
* │ 素材库 │ 预览 │ 属性 │ 上半区
|
|
25
|
+
* ├──────────┴──────────────┴─────────────────┤
|
|
26
|
+
* │ 时间轴(横跨整行) │ 下栏
|
|
27
|
+
* └───────────────────────────────────────────┘
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* 时间轴必须是**通栏**的:它曾被塞进中间列,于是左右面板一拉宽就把时间轴
|
|
31
|
+
* 挤窄,工具条上的按钮直接溢出到属性面板上(实测右栏拉宽时溢出 203px)。
|
|
32
|
+
* 与此同时,几乎所有 NLE 都是「轨道占满底部一条」,通栏也更符合预期。
|
|
49
33
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
34
|
+
* 尺寸一律用 inline style 的像素值(受控),配合 PanelResizer 的拖拽回写;
|
|
35
|
+
* 不用 tailwind 动态 class,避免被 purge 掉导致尺寸失效。
|
|
52
36
|
*/
|
|
53
37
|
function AppShell({ topBar, left, center, right, timeline, leftWidth, rightWidth, timelineHeight, leftResizer, rightResizer, timelineResizer, className }) {
|
|
54
38
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55
|
-
className: cn("flex h-full min-h-0 flex-col bg-background", className),
|
|
56
|
-
children: [
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
39
|
+
className: require_notify.cn("flex h-full min-h-0 flex-col bg-background", className),
|
|
40
|
+
children: [
|
|
41
|
+
topBar,
|
|
42
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
43
|
+
className: "flex min-h-0 flex-1 overflow-x-hidden overflow-y-auto",
|
|
44
|
+
children: [
|
|
45
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("aside", {
|
|
46
|
+
className: "flex min-h-[220px] shrink-0 flex-col overflow-hidden border-r bg-card/40",
|
|
47
|
+
style: { width: leftWidth },
|
|
48
|
+
"data-panel": "media",
|
|
49
|
+
children: left
|
|
50
|
+
}),
|
|
51
|
+
leftResizer,
|
|
52
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("main", {
|
|
53
|
+
className: "flex min-h-[220px] min-w-0 flex-1 flex-col overflow-hidden",
|
|
54
|
+
"data-panel": "preview",
|
|
55
|
+
children: center
|
|
56
|
+
}),
|
|
57
|
+
rightResizer,
|
|
58
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("aside", {
|
|
59
|
+
className: "flex min-h-[220px] shrink-0 flex-col overflow-hidden border-l bg-card/40",
|
|
60
|
+
style: { width: rightWidth },
|
|
61
|
+
"data-panel": "inspector",
|
|
62
|
+
children: right
|
|
63
|
+
})
|
|
64
|
+
]
|
|
65
|
+
}),
|
|
66
|
+
timelineResizer,
|
|
67
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
68
|
+
className: "flex min-h-0 shrink-0 flex-col overflow-hidden border-t bg-timeline-bg",
|
|
69
|
+
style: { height: timelineHeight },
|
|
70
|
+
"data-panel": "timeline",
|
|
71
|
+
children: timeline
|
|
72
|
+
})
|
|
73
|
+
]
|
|
88
74
|
});
|
|
89
75
|
}
|
|
90
76
|
// src/components/ui/button.tsx
|
|
@@ -120,7 +106,7 @@ function Button({ className, variant = "default", size = "default", asChild = fa
|
|
|
120
106
|
"data-slot": "button",
|
|
121
107
|
"data-variant": variant,
|
|
122
108
|
"data-size": size,
|
|
123
|
-
className: cn(buttonVariants({
|
|
109
|
+
className: require_notify.cn(buttonVariants({
|
|
124
110
|
variant,
|
|
125
111
|
size,
|
|
126
112
|
className
|
|
@@ -134,7 +120,7 @@ function Separator({ className, orientation = "horizontal", decorative = true, .
|
|
|
134
120
|
"data-slot": "separator",
|
|
135
121
|
decorative,
|
|
136
122
|
orientation,
|
|
137
|
-
className: cn("shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", className),
|
|
123
|
+
className: require_notify.cn("shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", className),
|
|
138
124
|
...props
|
|
139
125
|
});
|
|
140
126
|
}
|
|
@@ -213,33 +199,12 @@ function TooltipContent({ className, sideOffset = 0, children, ...props }) {
|
|
|
213
199
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Tooltip.Content, {
|
|
214
200
|
"data-slot": "tooltip-content",
|
|
215
201
|
sideOffset,
|
|
216
|
-
className: cn("z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", className),
|
|
202
|
+
className: require_notify.cn("z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", className),
|
|
217
203
|
...props,
|
|
218
204
|
children: [children, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Tooltip.Arrow, { className: "z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" })]
|
|
219
205
|
})
|
|
220
206
|
});
|
|
221
207
|
}
|
|
222
|
-
// src/components/ui/badge.tsx
|
|
223
|
-
var badgeVariants = (0, class_variance_authority.cva)("inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3", {
|
|
224
|
-
variants: { variant: {
|
|
225
|
-
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
|
226
|
-
secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
|
227
|
-
destructive: "bg-destructive text-destructive-foreground focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
|
|
228
|
-
outline: "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
|
229
|
-
ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
|
230
|
-
link: "text-primary underline-offset-4 [a&]:hover:underline"
|
|
231
|
-
} },
|
|
232
|
-
defaultVariants: { variant: "default" }
|
|
233
|
-
});
|
|
234
|
-
function Badge({ className, variant = "default", asChild = false, ...props }) {
|
|
235
|
-
const Comp = asChild ? radix_ui.Slot.Root : "span";
|
|
236
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Comp, {
|
|
237
|
-
"data-slot": "badge",
|
|
238
|
-
"data-variant": variant,
|
|
239
|
-
className: cn(badgeVariants({ variant }), className),
|
|
240
|
-
...props
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
208
|
// src/components/ui/popover.tsx
|
|
244
209
|
function Popover({ ...props }) {
|
|
245
210
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Popover.Root, {
|
|
@@ -261,7 +226,7 @@ function PopoverContent({ className, align = "center", sideOffset = 4, ...props
|
|
|
261
226
|
"data-slot": "popover-content",
|
|
262
227
|
align,
|
|
263
228
|
sideOffset,
|
|
264
|
-
className: cn("z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", className),
|
|
229
|
+
className: require_notify.cn("z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", className),
|
|
265
230
|
...props
|
|
266
231
|
})
|
|
267
232
|
});
|
|
@@ -272,7 +237,7 @@ function CapabilityBadge({ info }) {
|
|
|
272
237
|
const { status, report, summary } = info;
|
|
273
238
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Popover, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PopoverTrigger, {
|
|
274
239
|
asChild: true,
|
|
275
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Badge, {
|
|
240
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_notify.Badge, {
|
|
276
241
|
variant: "outline",
|
|
277
242
|
className: "cursor-pointer gap-1 text-[11px] font-normal select-none",
|
|
278
243
|
children: [status === "checking" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Loader2, { className: "size-3 animate-spin" }) : status === "ready" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.CheckCircle2, { className: "size-3 text-success" }) : status === "degraded" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.AlertTriangle, { className: "size-3 text-warning" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.XCircle, { className: "size-3 text-destructive" }), summary]
|
|
@@ -341,44 +306,6 @@ function Row$1({ label, ok, optional }) {
|
|
|
341
306
|
})]
|
|
342
307
|
});
|
|
343
308
|
}
|
|
344
|
-
// src/context.tsx
|
|
345
|
-
/**
|
|
346
|
-
* 工作台自身的 Context:UI 状态(面板尺寸 / 缩放 / 对话框)+ 宿主桥。
|
|
347
|
-
*
|
|
348
|
-
* 与 `@cut-workbench/react` 的编辑器 Context 分开:编辑器是「数据与引擎」,
|
|
349
|
-
* 这里是「某一次挂载的布局与交互」。两者生命周期可以不同(例如宿主切换项目时
|
|
350
|
-
* 复用同一个编辑器、只重建 UI)。
|
|
351
|
-
*/
|
|
352
|
-
var WorkbenchContext = (0, react.createContext)(null);
|
|
353
|
-
function useWorkbench() {
|
|
354
|
-
const value = (0, react.useContext)(WorkbenchContext);
|
|
355
|
-
if (value == null) throw new Error("[cut-workbench] 缺少 <WorkbenchProvider>:组件必须挂在工作台内部");
|
|
356
|
-
return value;
|
|
357
|
-
}
|
|
358
|
-
/** 订阅 UI 状态(带 selector) */
|
|
359
|
-
function useUi(selector) {
|
|
360
|
-
const { ui } = useWorkbench();
|
|
361
|
-
return (0, react.useSyncExternalStore)(ui.subscribe, () => selector(ui.getState()), () => selector(ui.getState()));
|
|
362
|
-
}
|
|
363
|
-
/** 取 UI store 本体(事件回调里读写用) */
|
|
364
|
-
function useUiStoreApi() {
|
|
365
|
-
return useWorkbench().ui;
|
|
366
|
-
}
|
|
367
|
-
/** 默认功能开关 */
|
|
368
|
-
var DEFAULT_FEATURES = {
|
|
369
|
-
mediaLibrary: true,
|
|
370
|
-
preview: true,
|
|
371
|
-
timeline: true,
|
|
372
|
-
inspector: true,
|
|
373
|
-
export: true,
|
|
374
|
-
canvasSettings: true,
|
|
375
|
-
lab: false
|
|
376
|
-
};
|
|
377
|
-
var DEFAULT_CONFIG = {
|
|
378
|
-
features: DEFAULT_FEATURES,
|
|
379
|
-
keyboard: "global",
|
|
380
|
-
resizable: true
|
|
381
|
-
};
|
|
382
309
|
// src/components/layout/WorkbenchToolbar.tsx
|
|
383
310
|
/**
|
|
384
311
|
* 内置顶栏(WorkbenchToolbar)。
|
|
@@ -387,59 +314,71 @@ var DEFAULT_CONFIG = {
|
|
|
387
314
|
* - 品牌区可替换(`brand`),不强制显示项目名;
|
|
388
315
|
* - 保存/打开是可选回调,宿主不提供就不渲染按钮;
|
|
389
316
|
* - 「视图切换(编辑器/实验室)」只在开启 lab 特性时显示。
|
|
317
|
+
*
|
|
318
|
+
* 可自定义的层次(由浅到深):
|
|
319
|
+
* 1. **文案**:`labels` 覆盖工程名占位、能力徽标等;
|
|
320
|
+
* 2. **局部**:`brand` / `title` / `status` / `actions` / `showCapabilityBadge`
|
|
321
|
+
* 逐块替换;
|
|
322
|
+
* 3. **整条**:`slots.topBar`(传 `null` 表示完全不要顶栏)。
|
|
390
323
|
*/
|
|
391
324
|
/** 顶栏:品牌 / 工程名 / 撤销重做 / 保存 / 导出 / 能力徽标 / 视图切换 */
|
|
392
|
-
function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject, brand, showLab = false }) {
|
|
325
|
+
function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject, brand, title, status, actions, showCapabilityBadge = true, showLab = false, editableTitle = false, labels }) {
|
|
393
326
|
const projectName = (0, _cut_workbench_react.useEditor)((s) => s.project.name);
|
|
327
|
+
const renameProject = (0, _cut_workbench_react.useEditor)((s) => s.renameProject);
|
|
394
328
|
const dirty = (0, _cut_workbench_react.useEditor)((s) => s.dirty);
|
|
395
329
|
const undo = (0, _cut_workbench_react.useEditor)((s) => s.undo);
|
|
396
330
|
const redo = (0, _cut_workbench_react.useEditor)((s) => s.redo);
|
|
397
331
|
const canUndo = (0, _cut_workbench_react.useHistory)((s) => s.past.length > 0);
|
|
398
332
|
const canRedo = (0, _cut_workbench_react.useHistory)((s) => s.future.length > 0);
|
|
399
|
-
const route = useUi((s) => s.route);
|
|
400
|
-
const setRoute = useUi((s) => s.setRoute);
|
|
401
|
-
const openDialog = useUi((s) => s.openDialog);
|
|
333
|
+
const route = require_notify.useUi((s) => s.route);
|
|
334
|
+
const setRoute = require_notify.useUi((s) => s.setRoute);
|
|
335
|
+
const openDialog = require_notify.useUi((s) => s.openDialog);
|
|
402
336
|
const clipCount = (0, _cut_workbench_react.useEditor)((s) => s.project.clips.length);
|
|
403
337
|
const projectWidth = (0, _cut_workbench_react.useEditor)((s) => s.project.width);
|
|
404
338
|
const projectHeight = (0, _cut_workbench_react.useEditor)((s) => s.project.height);
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
339
|
+
const text = {
|
|
340
|
+
untitledProject: "未命名工程",
|
|
341
|
+
emptyProject: "空工程",
|
|
342
|
+
export: "导出",
|
|
343
|
+
canvasSettings: "画布设置",
|
|
344
|
+
showLab: "能力实验室",
|
|
345
|
+
backToEditor: "返回编辑器",
|
|
346
|
+
save: "保存工程",
|
|
347
|
+
open: "加载工程",
|
|
348
|
+
undo: "撤销",
|
|
349
|
+
redo: "重做",
|
|
350
|
+
...labels
|
|
351
|
+
};
|
|
352
|
+
const branding = brand == null ? null : brand;
|
|
353
|
+
const projectTitle = title === null ? null : title ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProjectTitle, {
|
|
354
|
+
name: projectName,
|
|
355
|
+
dirty,
|
|
356
|
+
placeholder: text.untitledProject,
|
|
357
|
+
editable: editableTitle,
|
|
358
|
+
onRename: renameProject
|
|
359
|
+
});
|
|
360
|
+
const statusNode = status === null ? null : status ?? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
361
|
+
className: "hidden text-xs text-muted-foreground sm:inline",
|
|
362
|
+
children: [
|
|
363
|
+
clipCount > 0 ? `${clipCount} 个片段` : text.emptyProject,
|
|
364
|
+
" ·",
|
|
365
|
+
" ",
|
|
366
|
+
(0, _cut_workbench_core.ratioLabel)(projectWidth, projectHeight)
|
|
367
|
+
]
|
|
414
368
|
});
|
|
415
369
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
416
|
-
className: "flex h-12 shrink-0 items-center gap-2 border-b bg-card px-3",
|
|
370
|
+
className: "flex h-12 shrink-0 items-center gap-2 overflow-hidden border-b bg-card px-3",
|
|
371
|
+
"data-workbench-toolbar": true,
|
|
417
372
|
children: [
|
|
418
373
|
branding,
|
|
419
|
-
branding != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
|
|
374
|
+
branding != null && projectTitle != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
|
|
420
375
|
orientation: "vertical",
|
|
421
376
|
className: "h-5"
|
|
422
377
|
}),
|
|
378
|
+
projectTitle,
|
|
379
|
+
statusNode,
|
|
423
380
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
424
|
-
className: "flex
|
|
425
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
426
|
-
className: "truncate text-sm font-medium",
|
|
427
|
-
children: projectName
|
|
428
|
-
}), dirty && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
429
|
-
className: "size-1.5 rounded-full bg-primary",
|
|
430
|
-
title: "未保存"
|
|
431
|
-
})]
|
|
432
|
-
}),
|
|
433
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
434
|
-
className: "hidden text-xs text-muted-foreground sm:inline",
|
|
435
|
-
children: [
|
|
436
|
-
clipCount > 0 ? `${clipCount} 个片段` : "空工程",
|
|
437
|
-
" · ",
|
|
438
|
-
(0, _cut_workbench_core.ratioLabel)(projectWidth, projectHeight)
|
|
439
|
-
]
|
|
440
|
-
}),
|
|
441
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
442
|
-
className: "ml-auto flex items-center gap-1",
|
|
381
|
+
className: "ml-auto flex shrink-0 items-center gap-1",
|
|
443
382
|
children: [
|
|
444
383
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
445
384
|
asChild: true,
|
|
@@ -448,10 +387,10 @@ function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject,
|
|
|
448
387
|
size: "icon-sm",
|
|
449
388
|
onClick: undo,
|
|
450
389
|
disabled: !canUndo,
|
|
451
|
-
"aria-label":
|
|
390
|
+
"aria-label": text.undo,
|
|
452
391
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Undo2, { className: "size-4" })
|
|
453
392
|
})
|
|
454
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
393
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(TooltipContent, { children: [text.undo, " (Ctrl+Z)"] })] }),
|
|
455
394
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
456
395
|
asChild: true,
|
|
457
396
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
@@ -459,10 +398,10 @@ function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject,
|
|
|
459
398
|
size: "icon-sm",
|
|
460
399
|
onClick: redo,
|
|
461
400
|
disabled: !canRedo,
|
|
462
|
-
"aria-label":
|
|
401
|
+
"aria-label": text.redo,
|
|
463
402
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Redo2, { className: "size-4" })
|
|
464
403
|
})
|
|
465
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
404
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(TooltipContent, { children: [text.redo, " (Ctrl+Shift+Z)"] })] }),
|
|
466
405
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
|
|
467
406
|
orientation: "vertical",
|
|
468
407
|
className: "mx-1 h-5"
|
|
@@ -473,29 +412,29 @@ function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject,
|
|
|
473
412
|
variant: "ghost",
|
|
474
413
|
size: "icon-sm",
|
|
475
414
|
onClick: onOpenProject,
|
|
476
|
-
"aria-label":
|
|
415
|
+
"aria-label": text.open,
|
|
477
416
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.FolderOpen, { className: "size-4" })
|
|
478
417
|
})
|
|
479
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children:
|
|
418
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: text.open })] }),
|
|
480
419
|
onSaveProject != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
481
420
|
asChild: true,
|
|
482
421
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
483
422
|
variant: "ghost",
|
|
484
423
|
size: "icon-sm",
|
|
485
424
|
onClick: onSaveProject,
|
|
486
|
-
"aria-label":
|
|
425
|
+
"aria-label": text.save,
|
|
487
426
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Save, { className: "size-4" })
|
|
488
427
|
})
|
|
489
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children:
|
|
428
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: text.save })] }),
|
|
490
429
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
|
|
491
430
|
variant: "ghost",
|
|
492
431
|
size: "sm",
|
|
493
432
|
className: "gap-1.5",
|
|
494
|
-
"aria-label":
|
|
433
|
+
"aria-label": text.canvasSettings,
|
|
495
434
|
onClick: () => openDialog("projectSettings"),
|
|
496
435
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Monitor, { className: "size-4" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
497
436
|
className: "hidden md:inline",
|
|
498
|
-
children:
|
|
437
|
+
children: text.canvasSettings
|
|
499
438
|
})]
|
|
500
439
|
}),
|
|
501
440
|
showLab && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
|
|
@@ -505,21 +444,67 @@ function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject,
|
|
|
505
444
|
onClick: () => setRoute(route === "lab" ? "editor" : "lab"),
|
|
506
445
|
children: [route === "lab" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Video, { className: "size-4" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.FlaskConical, { className: "size-4" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
507
446
|
className: "hidden md:inline",
|
|
508
|
-
children: route === "lab" ?
|
|
447
|
+
children: route === "lab" ? text.backToEditor : text.showLab
|
|
509
448
|
})]
|
|
510
449
|
}),
|
|
511
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapabilityBadge, { info: capability }),
|
|
450
|
+
showCapabilityBadge && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapabilityBadge, { info: capability }),
|
|
451
|
+
actions,
|
|
512
452
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
|
|
513
453
|
size: "sm",
|
|
514
454
|
className: "ml-1 gap-1.5",
|
|
515
455
|
onClick: onExport,
|
|
516
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Download, { className: "size-4" }),
|
|
456
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Download, { className: "size-4" }), text.export]
|
|
517
457
|
})
|
|
518
458
|
]
|
|
519
459
|
})
|
|
520
460
|
]
|
|
521
461
|
});
|
|
522
462
|
}
|
|
463
|
+
/**
|
|
464
|
+
* 工程名展示(可选双击重命名)。
|
|
465
|
+
*
|
|
466
|
+
* 重命名走编辑器 store 的 `renameProject`,因此会进撤销栈、并标记 dirty ——
|
|
467
|
+
* 直接改 DOM 或本地 state 会让「保存了什么」和「看到什么」对不上。
|
|
468
|
+
*/
|
|
469
|
+
function ProjectTitle({ name, dirty, placeholder, editable, onRename }) {
|
|
470
|
+
const [editing, setEditing] = (0, react.useState)(false);
|
|
471
|
+
const [draft, setDraft] = (0, react.useState)(name);
|
|
472
|
+
const commit = () => {
|
|
473
|
+
setEditing(false);
|
|
474
|
+
const next = draft.trim();
|
|
475
|
+
if (next.length > 0 && next !== name) onRename(next);
|
|
476
|
+
};
|
|
477
|
+
if (editing) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
478
|
+
autoFocus: true,
|
|
479
|
+
value: draft,
|
|
480
|
+
onChange: (e) => setDraft(e.target.value),
|
|
481
|
+
onBlur: commit,
|
|
482
|
+
onKeyDown: (e) => {
|
|
483
|
+
if (e.key === "Enter") commit();
|
|
484
|
+
if (e.key === "Escape") {
|
|
485
|
+
setDraft(name);
|
|
486
|
+
setEditing(false);
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
className: "h-7 w-48 min-w-0 rounded border bg-background px-1.5 text-sm outline-none focus:ring-1 focus:ring-ring",
|
|
490
|
+
"aria-label": "工程名称"
|
|
491
|
+
});
|
|
492
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
493
|
+
className: "flex min-w-0 items-center gap-1.5",
|
|
494
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
495
|
+
className: require_notify.cn("truncate text-sm font-medium", editable && "cursor-text"),
|
|
496
|
+
title: editable ? "双击重命名" : name.length > 0 ? name : placeholder,
|
|
497
|
+
onDoubleClick: editable ? () => {
|
|
498
|
+
setDraft(name.length > 0 ? name : "");
|
|
499
|
+
setEditing(true);
|
|
500
|
+
} : void 0,
|
|
501
|
+
children: name.length > 0 ? name : placeholder
|
|
502
|
+
}), dirty && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
503
|
+
className: "size-1.5 shrink-0 rounded-full bg-primary",
|
|
504
|
+
title: "未保存"
|
|
505
|
+
})]
|
|
506
|
+
});
|
|
507
|
+
}
|
|
523
508
|
// src/components/layout/use-panel-resize.ts
|
|
524
509
|
/**
|
|
525
510
|
* 面板拖拽调整。内部用 rAF 节流,松手才把最终值写回 store(避免每帧重渲染整棵树)。
|
|
@@ -598,44 +583,8 @@ function PanelResizer({ orientation, value, min, max, invert, onChange, classNam
|
|
|
598
583
|
"aria-orientation": isVertical ? "vertical" : "horizontal",
|
|
599
584
|
"aria-label": label,
|
|
600
585
|
onPointerDown,
|
|
601
|
-
className: cn("group relative z-20 shrink-0 touch-none transition-colors", isVertical ? "w-1 cursor-col-resize" : "h-1 cursor-row-resize", dragging ? "bg-primary/60" : "bg-border hover:bg-primary/40", className),
|
|
602
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: cn("absolute", isVertical ? "-left-1 -right-1 inset-y-0" : "-top-1 -bottom-1 inset-x-0") })
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
// src/components/ui/card.tsx
|
|
606
|
-
function Card({ className, ...props }) {
|
|
607
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
608
|
-
"data-slot": "card",
|
|
609
|
-
className: cn("flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm", className),
|
|
610
|
-
...props
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
function CardHeader({ className, ...props }) {
|
|
614
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
615
|
-
"data-slot": "card-header",
|
|
616
|
-
className: cn("@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", className),
|
|
617
|
-
...props
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
function CardTitle({ className, ...props }) {
|
|
621
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
622
|
-
"data-slot": "card-title",
|
|
623
|
-
className: cn("leading-none font-semibold", className),
|
|
624
|
-
...props
|
|
625
|
-
});
|
|
626
|
-
}
|
|
627
|
-
function CardDescription({ className, ...props }) {
|
|
628
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
629
|
-
"data-slot": "card-description",
|
|
630
|
-
className: cn("text-sm text-muted-foreground", className),
|
|
631
|
-
...props
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
function CardContent({ className, ...props }) {
|
|
635
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
636
|
-
"data-slot": "card-content",
|
|
637
|
-
className: cn("px-6", className),
|
|
638
|
-
...props
|
|
586
|
+
className: require_notify.cn("group relative z-20 shrink-0 touch-none transition-colors", isVertical ? "w-1 cursor-col-resize" : "h-1 cursor-row-resize", dragging ? "bg-primary/60" : "bg-border hover:bg-primary/40", className),
|
|
587
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: require_notify.cn("absolute", isVertical ? "-left-1 -right-1 inset-y-0" : "-top-1 -bottom-1 inset-x-0") })
|
|
639
588
|
});
|
|
640
589
|
}
|
|
641
590
|
// src/components/common/LoadingOverlay.tsx
|
|
@@ -643,7 +592,7 @@ function CardContent({ className, ...props }) {
|
|
|
643
592
|
function LoadingOverlay({ visible, title = "处理中…", description, progress, className, fullscreen, children }) {
|
|
644
593
|
if (!visible) return null;
|
|
645
594
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
646
|
-
className: cn("z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm", fullscreen ? "fixed inset-0" : "absolute inset-0", className),
|
|
595
|
+
className: require_notify.cn("z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm", fullscreen ? "fixed inset-0" : "absolute inset-0", className),
|
|
647
596
|
role: "status",
|
|
648
597
|
"aria-live": "polite",
|
|
649
598
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -705,7 +654,12 @@ var CHECKS = [
|
|
|
705
654
|
hint: "绿幕抠像(可选)"
|
|
706
655
|
}
|
|
707
656
|
];
|
|
708
|
-
/**
|
|
657
|
+
/**
|
|
658
|
+
* 不满足运行条件时的阻断页:逐项说明缺失能力,不做静默降级。
|
|
659
|
+
*
|
|
660
|
+
* 「依赖没装」与「浏览器不支持」要分开呈现 —— 前者是宿主的构建问题(可执行
|
|
661
|
+
* 安装命令解决),后者只能换浏览器。混在一起会让人对着一条装包报错去升浏览器。
|
|
662
|
+
*/
|
|
709
663
|
function UnsupportedBrowser({ report, loading, error }) {
|
|
710
664
|
if (loading) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
711
665
|
className: "relative flex min-h-screen items-center justify-center bg-background",
|
|
@@ -715,23 +669,46 @@ function UnsupportedBrowser({ report, loading, error }) {
|
|
|
715
669
|
fullscreen: false
|
|
716
670
|
})
|
|
717
671
|
});
|
|
672
|
+
const missing = report?.missingDependencies ?? [];
|
|
673
|
+
const dependenciesMissing = missing.length > 0;
|
|
718
674
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
719
675
|
className: "flex min-h-screen items-center justify-center bg-background p-6",
|
|
720
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
|
|
676
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_notify.Card, {
|
|
721
677
|
className: "w-full max-w-2xl",
|
|
722
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CardHeader, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
678
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_notify.CardHeader, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
723
679
|
className: "flex items-center gap-2 text-destructive",
|
|
724
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.AlertOctagon, { className: "size-5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardTitle, {
|
|
680
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.AlertOctagon, { className: "size-5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_notify.CardTitle, {
|
|
725
681
|
className: "text-base",
|
|
726
|
-
children: "当前浏览器无法运行本应用"
|
|
682
|
+
children: dependenciesMissing ? "缺少引擎依赖" : "当前浏览器无法运行本应用"
|
|
727
683
|
})]
|
|
728
684
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
729
685
|
className: "text-sm text-muted-foreground",
|
|
730
|
-
children: "WebAV 依赖 WebCodecs 与 OPFS,仅支持 Chromium 内核浏览器。请使用最新版 Chrome 或 Edge(非无痕模式)重新打开。"
|
|
731
|
-
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CardContent, {
|
|
686
|
+
children: dependenciesMissing ? "工作台的预览与导出依赖 WebAV(@webav/av-cliper、@webav/av-canvas)。当前项目里没有解析到它们。" : "WebAV 依赖 WebCodecs 与 OPFS,仅支持 Chromium 内核浏览器。请使用最新版 Chrome 或 Edge(非无痕模式)重新打开。"
|
|
687
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_notify.CardContent, {
|
|
732
688
|
className: "space-y-4",
|
|
733
689
|
children: [
|
|
734
|
-
|
|
690
|
+
dependenciesMissing && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
691
|
+
className: "space-y-2 rounded-md border border-destructive/40 bg-destructive/5 p-3",
|
|
692
|
+
children: [
|
|
693
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
694
|
+
className: "text-xs font-medium text-destructive",
|
|
695
|
+
children: ["未解析到:", missing.join("、")]
|
|
696
|
+
}),
|
|
697
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
698
|
+
className: "text-xs text-muted-foreground",
|
|
699
|
+
children: "在宿主项目里执行:"
|
|
700
|
+
}),
|
|
701
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
702
|
+
className: "overflow-x-auto rounded bg-muted px-3 py-2 text-xs",
|
|
703
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: _cut_workbench_core_engine.ENGINE_DEPENDENCIES_INSTALL_COMMAND })
|
|
704
|
+
}),
|
|
705
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
706
|
+
className: "text-xs text-muted-foreground",
|
|
707
|
+
children: "安装后需要重新跑一次构建 —— 缺失依赖只在构建期解析,浏览器里刷新页面没用。"
|
|
708
|
+
})
|
|
709
|
+
]
|
|
710
|
+
}),
|
|
711
|
+
report != null && !dependenciesMissing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
735
712
|
className: "grid gap-2 sm:grid-cols-2",
|
|
736
713
|
children: CHECKS.map((check) => {
|
|
737
714
|
const value = report[check.key];
|
|
@@ -801,7 +778,7 @@ var Toaster = (props) => {
|
|
|
801
778
|
function Textarea({ className, ...props }) {
|
|
802
779
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
803
780
|
"data-slot": "textarea",
|
|
804
|
-
className: cn("flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40", className),
|
|
781
|
+
className: require_notify.cn("flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40", className),
|
|
805
782
|
...props
|
|
806
783
|
});
|
|
807
784
|
}
|
|
@@ -809,7 +786,7 @@ function Textarea({ className, ...props }) {
|
|
|
809
786
|
function Label({ className, ...props }) {
|
|
810
787
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Label.Root, {
|
|
811
788
|
"data-slot": "label",
|
|
812
|
-
className: cn("flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", className),
|
|
789
|
+
className: require_notify.cn("flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", className),
|
|
813
790
|
...props
|
|
814
791
|
});
|
|
815
792
|
}
|
|
@@ -817,7 +794,7 @@ function Label({ className, ...props }) {
|
|
|
817
794
|
function ScrollArea({ className, children, ...props }) {
|
|
818
795
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.ScrollArea.Root, {
|
|
819
796
|
"data-slot": "scroll-area",
|
|
820
|
-
className: cn("relative", className),
|
|
797
|
+
className: require_notify.cn("relative", className),
|
|
821
798
|
...props,
|
|
822
799
|
children: [
|
|
823
800
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ScrollArea.Viewport, {
|
|
@@ -834,7 +811,7 @@ function ScrollBar({ className, orientation = "vertical", ...props }) {
|
|
|
834
811
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ScrollArea.ScrollAreaScrollbar, {
|
|
835
812
|
"data-slot": "scroll-area-scrollbar",
|
|
836
813
|
orientation,
|
|
837
|
-
className: cn("flex touch-none p-px transition-colors select-none", orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent", orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent", className),
|
|
814
|
+
className: require_notify.cn("flex touch-none p-px transition-colors select-none", orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent", orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent", className),
|
|
838
815
|
...props,
|
|
839
816
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ScrollArea.ScrollAreaThumb, {
|
|
840
817
|
"data-slot": "scroll-area-thumb",
|
|
@@ -873,7 +850,7 @@ function DialogPortal({ ...props }) {
|
|
|
873
850
|
function DialogOverlay({ className, ...props }) {
|
|
874
851
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Dialog.Overlay, {
|
|
875
852
|
"data-slot": "dialog-overlay",
|
|
876
|
-
className: cn("fixed inset-0 z-50 bg-overlay data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0", className),
|
|
853
|
+
className: require_notify.cn("fixed inset-0 z-50 bg-overlay data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0", className),
|
|
877
854
|
...props
|
|
878
855
|
});
|
|
879
856
|
}
|
|
@@ -882,7 +859,7 @@ function DialogContent({ className, children, showCloseButton = true, ...props }
|
|
|
882
859
|
"data-slot": "dialog-portal",
|
|
883
860
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogOverlay, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Dialog.Content, {
|
|
884
861
|
"data-slot": "dialog-content",
|
|
885
|
-
className: cn("fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg", className),
|
|
862
|
+
className: require_notify.cn("fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg", className),
|
|
886
863
|
...props,
|
|
887
864
|
children: [children, showCloseButton && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Dialog.Close, {
|
|
888
865
|
"data-slot": "dialog-close",
|
|
@@ -898,14 +875,14 @@ function DialogContent({ className, children, showCloseButton = true, ...props }
|
|
|
898
875
|
function DialogHeader({ className, ...props }) {
|
|
899
876
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
900
877
|
"data-slot": "dialog-header",
|
|
901
|
-
className: cn("flex flex-col gap-2 text-center sm:text-left", className),
|
|
878
|
+
className: require_notify.cn("flex flex-col gap-2 text-center sm:text-left", className),
|
|
902
879
|
...props
|
|
903
880
|
});
|
|
904
881
|
}
|
|
905
882
|
function DialogFooter({ className, showCloseButton = false, children, ...props }) {
|
|
906
883
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
907
884
|
"data-slot": "dialog-footer",
|
|
908
|
-
className: cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className),
|
|
885
|
+
className: require_notify.cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className),
|
|
909
886
|
...props,
|
|
910
887
|
children: [children, showCloseButton && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Dialog.Close, {
|
|
911
888
|
asChild: true,
|
|
@@ -919,14 +896,14 @@ function DialogFooter({ className, showCloseButton = false, children, ...props }
|
|
|
919
896
|
function DialogTitle({ className, ...props }) {
|
|
920
897
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Dialog.Title, {
|
|
921
898
|
"data-slot": "dialog-title",
|
|
922
|
-
className: cn("text-lg leading-none font-semibold", className),
|
|
899
|
+
className: require_notify.cn("text-lg leading-none font-semibold", className),
|
|
923
900
|
...props
|
|
924
901
|
});
|
|
925
902
|
}
|
|
926
903
|
function DialogDescription({ className, ...props }) {
|
|
927
904
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Dialog.Description, {
|
|
928
905
|
"data-slot": "dialog-description",
|
|
929
|
-
className: cn("text-sm text-muted-foreground", className),
|
|
906
|
+
className: require_notify.cn("text-sm text-muted-foreground", className),
|
|
930
907
|
...props
|
|
931
908
|
});
|
|
932
909
|
}
|
|
@@ -955,7 +932,7 @@ function DropdownMenuContent({ className, sideOffset = 4, ...props }) {
|
|
|
955
932
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuPortal, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.DropdownMenu.Content, {
|
|
956
933
|
"data-slot": "dropdown-menu-content",
|
|
957
934
|
sideOffset,
|
|
958
|
-
className: cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", className),
|
|
935
|
+
className: require_notify.cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", className),
|
|
959
936
|
...props
|
|
960
937
|
}) });
|
|
961
938
|
}
|
|
@@ -964,7 +941,7 @@ function DropdownMenuItem({ className, inset, variant = "default", ...props }) {
|
|
|
964
941
|
"data-slot": "dropdown-menu-item",
|
|
965
942
|
"data-inset": inset,
|
|
966
943
|
"data-variant": variant,
|
|
967
|
-
className: cn("relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!", className),
|
|
944
|
+
className: require_notify.cn("relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!", className),
|
|
968
945
|
...props
|
|
969
946
|
});
|
|
970
947
|
}
|
|
@@ -972,14 +949,14 @@ function DropdownMenuLabel({ className, inset, ...props }) {
|
|
|
972
949
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.DropdownMenu.Label, {
|
|
973
950
|
"data-slot": "dropdown-menu-label",
|
|
974
951
|
"data-inset": inset,
|
|
975
|
-
className: cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className),
|
|
952
|
+
className: require_notify.cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className),
|
|
976
953
|
...props
|
|
977
954
|
});
|
|
978
955
|
}
|
|
979
956
|
function DropdownMenuSeparator({ className, ...props }) {
|
|
980
957
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.DropdownMenu.Separator, {
|
|
981
958
|
"data-slot": "dropdown-menu-separator",
|
|
982
|
-
className: cn("-mx-1 my-1 h-px bg-border", className),
|
|
959
|
+
className: require_notify.cn("-mx-1 my-1 h-px bg-border", className),
|
|
983
960
|
...props
|
|
984
961
|
});
|
|
985
962
|
}
|
|
@@ -987,7 +964,7 @@ function DropdownMenuSeparator({ className, ...props }) {
|
|
|
987
964
|
/** 统一空态:图标 + 标题 + 说明 + 可选操作 */
|
|
988
965
|
function EmptyState({ icon: Icon, title, description, action, className, compact }) {
|
|
989
966
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
990
|
-
className: cn("flex flex-col items-center justify-center gap-2 text-center", compact ? "px-4 py-6" : "px-6 py-12", className),
|
|
967
|
+
className: require_notify.cn("flex flex-col items-center justify-center gap-2 text-center", compact ? "px-4 py-6" : "px-6 py-12", className),
|
|
991
968
|
children: [
|
|
992
969
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
993
970
|
className: "flex size-10 items-center justify-center rounded-full bg-muted text-muted-foreground",
|
|
@@ -1005,43 +982,164 @@ function EmptyState({ icon: Icon, title, description, action, className, compact
|
|
|
1005
982
|
]
|
|
1006
983
|
});
|
|
1007
984
|
}
|
|
1008
|
-
// src/lib/
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
"
|
|
1014
|
-
"
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
985
|
+
// src/features/media-library/lib/pick-files.ts
|
|
986
|
+
var INPUT_ID = "cut-workbench-file-input";
|
|
987
|
+
/** 内置回退:隐藏 input,选完即弃 */
|
|
988
|
+
function pickWithInput(accept, multiple) {
|
|
989
|
+
return new Promise((resolve) => {
|
|
990
|
+
const input = document.createElement("input");
|
|
991
|
+
input.type = "file";
|
|
992
|
+
input.id = INPUT_ID;
|
|
993
|
+
input.multiple = multiple;
|
|
994
|
+
if (accept.length > 0) input.accept = accept.join(",");
|
|
995
|
+
input.style.display = "none";
|
|
996
|
+
document.body.appendChild(input);
|
|
997
|
+
const finish = (files) => {
|
|
998
|
+
input.remove();
|
|
999
|
+
resolve(files);
|
|
1000
|
+
};
|
|
1001
|
+
input.addEventListener("change", () => finish(Array.from(input.files ?? [])), { once: true });
|
|
1002
|
+
window.addEventListener("focus", () => {
|
|
1003
|
+
window.setTimeout(() => {
|
|
1004
|
+
if (document.body.contains(input)) finish([]);
|
|
1005
|
+
}, 300);
|
|
1006
|
+
}, { once: true });
|
|
1007
|
+
input.click();
|
|
1008
|
+
});
|
|
1029
1009
|
}
|
|
1030
|
-
/**
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1010
|
+
/**
|
|
1011
|
+
* @returns 选中的文件;用户取消时为空数组
|
|
1012
|
+
*/
|
|
1013
|
+
async function pickMediaFiles(bridge, options) {
|
|
1014
|
+
const multiple = options.multiple ?? true;
|
|
1015
|
+
if (bridge?.files?.pickFiles != null) {
|
|
1016
|
+
const result = await bridge.files.pickFiles({
|
|
1017
|
+
accept: options.accept,
|
|
1018
|
+
multiple,
|
|
1019
|
+
description: options.description
|
|
1020
|
+
});
|
|
1021
|
+
if (result != null) return result;
|
|
1022
|
+
}
|
|
1023
|
+
return pickWithInput(options.accept, multiple);
|
|
1034
1024
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1025
|
+
// src/features/media-library/hooks/useMediaImport.ts
|
|
1026
|
+
var ACCEPT = [
|
|
1027
|
+
"video/mp4",
|
|
1028
|
+
"audio/*",
|
|
1029
|
+
"image/*",
|
|
1030
|
+
".srt",
|
|
1031
|
+
"text/plain"
|
|
1032
|
+
];
|
|
1033
|
+
/**
|
|
1034
|
+
* 找出可以「重连」的素材。
|
|
1035
|
+
*
|
|
1036
|
+
* 场景:刷新后工程从 IndexedDB 恢复,但素材二进制消失(File 无法持久化)。
|
|
1037
|
+
* 用户重新导入同一个文件时,复用原 assetId,时间轴上已有片段立即恢复。
|
|
1038
|
+
* 匹配条件:同名 + 同类型 + 当前确实缺少二进制。
|
|
1039
|
+
*/
|
|
1040
|
+
function findReconnectableAssetId(state, file, registry) {
|
|
1041
|
+
if (state.missingAssetIds.length === 0) return null;
|
|
1042
|
+
return state.assets.find((asset) => state.missingAssetIds.includes(asset.id) && asset.name === file.name && registry.getFile(asset.id) == null)?.id ?? null;
|
|
1038
1043
|
}
|
|
1039
|
-
/**
|
|
1040
|
-
|
|
1041
|
-
|
|
1044
|
+
/**
|
|
1045
|
+
* 批量导入素材。
|
|
1046
|
+
*
|
|
1047
|
+
* - 串行处理,避免同时开辟多个解码器把内存打爆;
|
|
1048
|
+
* - 单个文件失败只影响自身,最后汇总 toast;
|
|
1049
|
+
* - 进度写在本地 state(不放进 store,避免高频更新引起全局重渲染);
|
|
1050
|
+
* - 若与「缺失源文件」的素材同名同类型,则重连而不是新增。
|
|
1051
|
+
*/
|
|
1052
|
+
function useMediaImport() {
|
|
1053
|
+
const editor = (0, _cut_workbench_react.useCutEditor)();
|
|
1054
|
+
const clips = editor.runtime.clips;
|
|
1055
|
+
const store = editor.store;
|
|
1056
|
+
const [progress, setProgress] = (0, react.useState)([]);
|
|
1057
|
+
const [importing, setImporting] = (0, react.useState)(false);
|
|
1058
|
+
const addAsset = (0, _cut_workbench_react.useEditor)((s) => s.addAsset);
|
|
1059
|
+
const replaceAsset = (0, _cut_workbench_react.useEditor)((s) => s.replaceAsset);
|
|
1060
|
+
const clearMissing = (0, _cut_workbench_react.useEditor)((s) => s.clearMissing);
|
|
1061
|
+
const selectAsset = (0, _cut_workbench_react.useEditor)((s) => s.selectAsset);
|
|
1062
|
+
const beginImport = require_notify.useUi((s) => s.beginImport);
|
|
1063
|
+
const endImport = require_notify.useUi((s) => s.endImport);
|
|
1064
|
+
const { bridge } = require_notify.useWorkbench();
|
|
1065
|
+
return {
|
|
1066
|
+
importing,
|
|
1067
|
+
progress,
|
|
1068
|
+
importFiles: (0, react.useCallback)(async (files) => {
|
|
1069
|
+
if (files.length === 0) return [];
|
|
1070
|
+
setImporting(true);
|
|
1071
|
+
beginImport();
|
|
1072
|
+
setProgress(files.map((f) => ({
|
|
1073
|
+
name: f.name,
|
|
1074
|
+
progress: 0
|
|
1075
|
+
})));
|
|
1076
|
+
const imported = [];
|
|
1077
|
+
const failures = [];
|
|
1078
|
+
let reconnected = 0;
|
|
1079
|
+
for (let i = 0; i < files.length; i += 1) {
|
|
1080
|
+
const file = files[i];
|
|
1081
|
+
if (!file) continue;
|
|
1082
|
+
try {
|
|
1083
|
+
const reconnectId = findReconnectableAssetId(store.getState(), file, clips);
|
|
1084
|
+
const result = await (0, _cut_workbench_core_engine.importFile)(clips, file, (p) => {
|
|
1085
|
+
setProgress((prev) => prev.map((item, index) => index === i ? {
|
|
1086
|
+
...item,
|
|
1087
|
+
progress: p
|
|
1088
|
+
} : item));
|
|
1089
|
+
});
|
|
1090
|
+
if (reconnectId != null) {
|
|
1091
|
+
clips.replaceAssetSource(reconnectId, file, result.master ?? null);
|
|
1092
|
+
replaceAsset(reconnectId, result.asset);
|
|
1093
|
+
clearMissing(reconnectId);
|
|
1094
|
+
imported.push({
|
|
1095
|
+
...result.asset,
|
|
1096
|
+
id: reconnectId
|
|
1097
|
+
});
|
|
1098
|
+
reconnected += 1;
|
|
1099
|
+
} else {
|
|
1100
|
+
addAsset(result.asset);
|
|
1101
|
+
imported.push(result.asset);
|
|
1102
|
+
}
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
const message = err instanceof _cut_workbench_core_engine.UnsupportedFileError ? err.message : err instanceof Error ? err.message : String(err);
|
|
1105
|
+
failures.push(`${file.name}:${message}`);
|
|
1106
|
+
setProgress((prev) => prev.map((item, index) => index === i ? {
|
|
1107
|
+
...item,
|
|
1108
|
+
error: message
|
|
1109
|
+
} : item));
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
setImporting(false);
|
|
1113
|
+
endImport();
|
|
1114
|
+
if (imported.length > 0) {
|
|
1115
|
+
selectAsset(imported[0]?.id ?? null);
|
|
1116
|
+
require_notify.notify(bridge, {
|
|
1117
|
+
level: "success",
|
|
1118
|
+
title: reconnected > 0 ? `已导入 ${imported.length} 个素材(其中 ${reconnected} 个重连到已有片段)` : `已导入 ${imported.length} 个素材`
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
if (failures.length > 0) require_notify.notify(bridge, {
|
|
1122
|
+
level: "error",
|
|
1123
|
+
title: `${failures.length} 个文件导入失败`,
|
|
1124
|
+
description: failures.slice(0, 3).join("\n"),
|
|
1125
|
+
duration: 8e3
|
|
1126
|
+
});
|
|
1127
|
+
window.setTimeout(() => setProgress([]), 3e3);
|
|
1128
|
+
return imported;
|
|
1129
|
+
}, [
|
|
1130
|
+
addAsset,
|
|
1131
|
+
beginImport,
|
|
1132
|
+
bridge,
|
|
1133
|
+
clearMissing,
|
|
1134
|
+
endImport,
|
|
1135
|
+
replaceAsset,
|
|
1136
|
+
selectAsset
|
|
1137
|
+
])
|
|
1138
|
+
};
|
|
1042
1139
|
}
|
|
1043
1140
|
// src/features/media-library/components/DropZone.tsx
|
|
1044
|
-
|
|
1141
|
+
/** react-dropzone 用的 MIME → 扩展名映射 */
|
|
1142
|
+
var DROPZONE_ACCEPT = {
|
|
1045
1143
|
"video/mp4": [".mp4", ".m4v"],
|
|
1046
1144
|
"audio/*": [
|
|
1047
1145
|
".mp3",
|
|
@@ -1064,28 +1162,51 @@ var ACCEPT = {
|
|
|
1064
1162
|
/**
|
|
1065
1163
|
* 拖拽 / 点击导入区域。
|
|
1066
1164
|
*
|
|
1165
|
+
* 两条入口:
|
|
1166
|
+
* - **拖拽**:走 react-dropzone(宿主无法介入,这是浏览器原生行为);
|
|
1167
|
+
* - **点击**:优先走 `HostBridge.files.pickFiles` —— 宿主可能不用系统文件
|
|
1168
|
+
* 对话框,而是从自己的素材库 / 云端 / URL 导入。宿主返回 `null`(不接管)
|
|
1169
|
+
* 时才回退到内置 `<input type=file>`。
|
|
1170
|
+
*
|
|
1067
1171
|
* 注意:react-dropzone 的 accept 只做初筛,真正按 MIME + 扩展名校验在
|
|
1068
1172
|
* core/media/file-reader,从而保证「拖入 .webm 得到明确错误」而不是被静默拒绝。
|
|
1069
1173
|
*/
|
|
1070
1174
|
function DropZone({ onFiles, disabled, className, compact }) {
|
|
1071
|
-
const {
|
|
1175
|
+
const { bridge } = require_notify.useWorkbench();
|
|
1176
|
+
const { getRootProps, getInputProps, isDragActive } = (0, react_dropzone.useDropzone)({
|
|
1072
1177
|
onDrop: (accepted) => {
|
|
1073
1178
|
if (accepted.length > 0) onFiles(accepted);
|
|
1074
1179
|
},
|
|
1075
|
-
noClick:
|
|
1076
|
-
noKeyboard:
|
|
1180
|
+
noClick: true,
|
|
1181
|
+
noKeyboard: true,
|
|
1077
1182
|
multiple: true,
|
|
1078
1183
|
disabled,
|
|
1079
|
-
accept:
|
|
1080
|
-
});
|
|
1184
|
+
accept: DROPZONE_ACCEPT
|
|
1185
|
+
});
|
|
1186
|
+
const handleClick = (0, react.useCallback)(() => {
|
|
1187
|
+
if (disabled) return;
|
|
1188
|
+
pickMediaFiles(bridge, {
|
|
1189
|
+
accept: ACCEPT,
|
|
1190
|
+
multiple: true,
|
|
1191
|
+
description: "选择要导入的素材"
|
|
1192
|
+
}).then((files) => {
|
|
1193
|
+
if (files.length > 0) onFiles(files);
|
|
1194
|
+
});
|
|
1195
|
+
}, [
|
|
1196
|
+
bridge,
|
|
1197
|
+
disabled,
|
|
1198
|
+
onFiles
|
|
1199
|
+
]);
|
|
1081
1200
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1082
1201
|
...getRootProps(),
|
|
1083
|
-
|
|
1202
|
+
onClick: handleClick,
|
|
1203
|
+
"data-drop-zone": "",
|
|
1204
|
+
className: require_notify.cn("group relative flex cursor-pointer flex-col items-center justify-center gap-1.5 rounded-lg border border-dashed text-center transition-colors", compact ? "px-3 py-2.5" : "px-4 py-8", isDragActive ? "border-primary bg-primary/10" : "border-border hover:border-primary/60 hover:bg-muted/40", disabled && "pointer-events-none opacity-50", className),
|
|
1084
1205
|
children: [
|
|
1085
1206
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", { ...getInputProps() }),
|
|
1086
1207
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1087
1208
|
className: "flex items-center gap-2 text-sm font-medium",
|
|
1088
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Upload, { className: cn("text-muted-foreground", compact ? "size-3.5" : "size-4") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: compact ? "导入素材" : isDragActive ? "松开以导入" : "拖入文件,或点击选择" })]
|
|
1209
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Upload, { className: require_notify.cn("text-muted-foreground", compact ? "size-3.5" : "size-4") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: compact ? "导入素材" : isDragActive ? "松开以导入" : "拖入文件,或点击选择" })]
|
|
1089
1210
|
}),
|
|
1090
1211
|
!compact && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1091
1212
|
className: "text-xs text-muted-foreground",
|
|
@@ -1096,7 +1217,7 @@ function DropZone({ onFiles, disabled, className, compact }) {
|
|
|
1096
1217
|
className: "sr-only",
|
|
1097
1218
|
onClick: (e) => {
|
|
1098
1219
|
e.stopPropagation();
|
|
1099
|
-
|
|
1220
|
+
handleClick();
|
|
1100
1221
|
},
|
|
1101
1222
|
children: "选择文件"
|
|
1102
1223
|
})
|
|
@@ -1107,7 +1228,7 @@ function DropZone({ onFiles, disabled, className, compact }) {
|
|
|
1107
1228
|
function Skeleton({ className, ...props }) {
|
|
1108
1229
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1109
1230
|
"data-slot": "skeleton",
|
|
1110
|
-
className: cn("animate-pulse rounded-md bg-accent", className),
|
|
1231
|
+
className: require_notify.cn("animate-pulse rounded-md bg-accent", className),
|
|
1111
1232
|
...props
|
|
1112
1233
|
});
|
|
1113
1234
|
}
|
|
@@ -1156,7 +1277,7 @@ function MediaCard({ asset, selected, missing, onSelect, onAddToTimeline, onShow
|
|
|
1156
1277
|
onKeyDown: (e) => {
|
|
1157
1278
|
if (e.key === "Enter") onAddToTimeline();
|
|
1158
1279
|
},
|
|
1159
|
-
className: cn("group relative flex cursor-pointer flex-col gap-1.5 rounded-lg border p-1.5 text-left transition-colors outline-none", missing ? "border-warning/50 bg-warning/5" : selected ? "border-primary bg-primary/10 ring-1 ring-primary/40" : "border-border bg-card hover:border-primary/40 hover:bg-muted/40 focus-visible:border-primary"),
|
|
1280
|
+
className: require_notify.cn("group relative flex cursor-pointer flex-col gap-1.5 rounded-lg border p-1.5 text-left transition-colors outline-none", missing ? "border-warning/50 bg-warning/5" : selected ? "border-primary bg-primary/10 ring-1 ring-primary/40" : "border-border bg-card hover:border-primary/40 hover:bg-muted/40 focus-visible:border-primary"),
|
|
1160
1281
|
children: [
|
|
1161
1282
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1162
1283
|
className: "relative aspect-video w-full overflow-hidden rounded-md bg-muted",
|
|
@@ -1177,7 +1298,7 @@ function MediaCard({ asset, selected, missing, onSelect, onAddToTimeline, onShow
|
|
|
1177
1298
|
opacity: .9
|
|
1178
1299
|
})
|
|
1179
1300
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1180
|
-
className: cn("flex size-full items-center justify-center", kind.tone),
|
|
1301
|
+
className: require_notify.cn("flex size-full items-center justify-center", kind.tone),
|
|
1181
1302
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: "size-7" })
|
|
1182
1303
|
}),
|
|
1183
1304
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
@@ -1224,7 +1345,7 @@ function MediaCard({ asset, selected, missing, onSelect, onAddToTimeline, onShow
|
|
|
1224
1345
|
children: asset.name
|
|
1225
1346
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1226
1347
|
className: "truncate text-[10px] text-muted-foreground",
|
|
1227
|
-
children: [asset.kind === "audio" ? `${formatBytes(asset.size)} · 48kHz 双声道` : asset.kind === "text" || asset.kind === "subtitle" ? `${formatResolution(asset.width, asset.height)}` : `${formatResolution(asset.width, asset.height)} · ${formatBytes(asset.size)}`, asset.hasAudio ? " · 含音轨" : ""]
|
|
1348
|
+
children: [asset.kind === "audio" ? `${require_notify.formatBytes(asset.size)} · 48kHz 双声道` : asset.kind === "text" || asset.kind === "subtitle" ? `${require_notify.formatResolution(asset.width, asset.height)}` : `${require_notify.formatResolution(asset.width, asset.height)} · ${require_notify.formatBytes(asset.size)}`, asset.hasAudio ? " · 含音轨" : ""]
|
|
1228
1349
|
})]
|
|
1229
1350
|
}),
|
|
1230
1351
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DropdownMenu, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuTrigger, {
|
|
@@ -1266,7 +1387,7 @@ function MediaGrid({ assets, selectedAssetId, missingAssetIds = [], progress, on
|
|
|
1266
1387
|
className: "grid grid-cols-2 gap-2.5 p-2.5",
|
|
1267
1388
|
children: [
|
|
1268
1389
|
progress.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1269
|
-
className: cn("flex flex-col gap-1.5 rounded-lg border border-dashed p-1.5", item.error != null && "border-destructive/50 bg-destructive/5"),
|
|
1390
|
+
className: require_notify.cn("flex flex-col gap-1.5 rounded-lg border border-dashed p-1.5", item.error != null && "border-destructive/50 bg-destructive/5"),
|
|
1270
1391
|
children: [
|
|
1271
1392
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Skeleton, { className: "aspect-video w-full rounded-md" }),
|
|
1272
1393
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -1317,122 +1438,21 @@ function MediaGrid({ assets, selectedAssetId, missingAssetIds = [], progress, on
|
|
|
1317
1438
|
]
|
|
1318
1439
|
});
|
|
1319
1440
|
}
|
|
1320
|
-
// src/features/media-library/
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
function findReconnectableAssetId(state, file, registry) {
|
|
1329
|
-
if (state.missingAssetIds.length === 0) return null;
|
|
1330
|
-
return state.assets.find((asset) => state.missingAssetIds.includes(asset.id) && asset.name === file.name && registry.getFile(asset.id) == null)?.id ?? null;
|
|
1331
|
-
}
|
|
1441
|
+
// src/features/media-library/components/MediaLibraryPanel.tsx
|
|
1442
|
+
var KIND_LABEL$1 = {
|
|
1443
|
+
video: "视频",
|
|
1444
|
+
audio: "音频",
|
|
1445
|
+
image: "图片",
|
|
1446
|
+
text: "文字",
|
|
1447
|
+
subtitle: "字幕"
|
|
1448
|
+
};
|
|
1332
1449
|
/**
|
|
1333
|
-
*
|
|
1334
|
-
*
|
|
1335
|
-
* - 串行处理,避免同时开辟多个解码器把内存打爆;
|
|
1336
|
-
* - 单个文件失败只影响自身,最后汇总 toast;
|
|
1337
|
-
* - 进度写在本地 state(不放进 store,避免高频更新引起全局重渲染);
|
|
1338
|
-
* - 若与「缺失源文件」的素材同名同类型,则重连而不是新增。
|
|
1450
|
+
* 素材库面板:导入区 + 新建(文字/字幕)+ 网格 + 详情。
|
|
1339
1451
|
*/
|
|
1340
|
-
function
|
|
1341
|
-
const
|
|
1342
|
-
const
|
|
1343
|
-
const
|
|
1344
|
-
const [progress, setProgress] = (0, react.useState)([]);
|
|
1345
|
-
const [importing, setImporting] = (0, react.useState)(false);
|
|
1346
|
-
const addAsset = (0, _cut_workbench_react.useEditor)((s) => s.addAsset);
|
|
1347
|
-
const replaceAsset = (0, _cut_workbench_react.useEditor)((s) => s.replaceAsset);
|
|
1348
|
-
const clearMissing = (0, _cut_workbench_react.useEditor)((s) => s.clearMissing);
|
|
1349
|
-
const selectAsset = (0, _cut_workbench_react.useEditor)((s) => s.selectAsset);
|
|
1350
|
-
const beginImport = useUi((s) => s.beginImport);
|
|
1351
|
-
const endImport = useUi((s) => s.endImport);
|
|
1352
|
-
return {
|
|
1353
|
-
importing,
|
|
1354
|
-
progress,
|
|
1355
|
-
importFiles: (0, react.useCallback)(async (files) => {
|
|
1356
|
-
if (files.length === 0) return [];
|
|
1357
|
-
setImporting(true);
|
|
1358
|
-
beginImport();
|
|
1359
|
-
setProgress(files.map((f) => ({
|
|
1360
|
-
name: f.name,
|
|
1361
|
-
progress: 0
|
|
1362
|
-
})));
|
|
1363
|
-
const imported = [];
|
|
1364
|
-
const failures = [];
|
|
1365
|
-
let reconnected = 0;
|
|
1366
|
-
for (let i = 0; i < files.length; i += 1) {
|
|
1367
|
-
const file = files[i];
|
|
1368
|
-
if (!file) continue;
|
|
1369
|
-
try {
|
|
1370
|
-
const reconnectId = findReconnectableAssetId(store.getState(), file, clips);
|
|
1371
|
-
const result = await (0, _cut_workbench_core_engine.importFile)(clips, file, (p) => {
|
|
1372
|
-
setProgress((prev) => prev.map((item, index) => index === i ? {
|
|
1373
|
-
...item,
|
|
1374
|
-
progress: p
|
|
1375
|
-
} : item));
|
|
1376
|
-
});
|
|
1377
|
-
if (reconnectId != null) {
|
|
1378
|
-
clips.replaceAssetSource(reconnectId, file, result.master ?? null);
|
|
1379
|
-
replaceAsset(reconnectId, result.asset);
|
|
1380
|
-
clearMissing(reconnectId);
|
|
1381
|
-
imported.push({
|
|
1382
|
-
...result.asset,
|
|
1383
|
-
id: reconnectId
|
|
1384
|
-
});
|
|
1385
|
-
reconnected += 1;
|
|
1386
|
-
} else {
|
|
1387
|
-
addAsset(result.asset);
|
|
1388
|
-
imported.push(result.asset);
|
|
1389
|
-
}
|
|
1390
|
-
} catch (err) {
|
|
1391
|
-
const message = err instanceof _cut_workbench_core_engine.UnsupportedFileError ? err.message : err instanceof Error ? err.message : String(err);
|
|
1392
|
-
failures.push(`${file.name}:${message}`);
|
|
1393
|
-
setProgress((prev) => prev.map((item, index) => index === i ? {
|
|
1394
|
-
...item,
|
|
1395
|
-
error: message
|
|
1396
|
-
} : item));
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
setImporting(false);
|
|
1400
|
-
endImport();
|
|
1401
|
-
if (imported.length > 0) {
|
|
1402
|
-
selectAsset(imported[0]?.id ?? null);
|
|
1403
|
-
sonner.toast.success(reconnected > 0 ? `已导入 ${imported.length} 个素材(其中 ${reconnected} 个重连到已有片段)` : `已导入 ${imported.length} 个素材`);
|
|
1404
|
-
}
|
|
1405
|
-
if (failures.length > 0) sonner.toast.error(`${failures.length} 个文件导入失败`, {
|
|
1406
|
-
description: failures.slice(0, 3).join("\n"),
|
|
1407
|
-
duration: 8e3
|
|
1408
|
-
});
|
|
1409
|
-
window.setTimeout(() => setProgress([]), 3e3);
|
|
1410
|
-
return imported;
|
|
1411
|
-
}, [
|
|
1412
|
-
addAsset,
|
|
1413
|
-
beginImport,
|
|
1414
|
-
clearMissing,
|
|
1415
|
-
endImport,
|
|
1416
|
-
replaceAsset,
|
|
1417
|
-
selectAsset
|
|
1418
|
-
])
|
|
1419
|
-
};
|
|
1420
|
-
}
|
|
1421
|
-
// src/features/media-library/components/MediaLibraryPanel.tsx
|
|
1422
|
-
var KIND_LABEL$1 = {
|
|
1423
|
-
video: "视频",
|
|
1424
|
-
audio: "音频",
|
|
1425
|
-
image: "图片",
|
|
1426
|
-
text: "文字",
|
|
1427
|
-
subtitle: "字幕"
|
|
1428
|
-
};
|
|
1429
|
-
/**
|
|
1430
|
-
* 素材库面板:导入区 + 新建(文字/字幕)+ 网格 + 详情。
|
|
1431
|
-
*/
|
|
1432
|
-
function MediaLibraryPanel({ onAddToTimeline, onRemoveAsset }) {
|
|
1433
|
-
const assets = (0, _cut_workbench_react.useEditor)((s) => s.assets);
|
|
1434
|
-
const selectedAssetId = (0, _cut_workbench_react.useEditor)((s) => s.selectedAssetId);
|
|
1435
|
-
const missingAssetIds = (0, _cut_workbench_react.useEditor)((s) => s.missingAssetIds);
|
|
1452
|
+
function MediaLibraryPanel({ onAddToTimeline, onRemoveAsset }) {
|
|
1453
|
+
const assets = (0, _cut_workbench_react.useEditor)((s) => s.assets);
|
|
1454
|
+
const selectedAssetId = (0, _cut_workbench_react.useEditor)((s) => s.selectedAssetId);
|
|
1455
|
+
const missingAssetIds = (0, _cut_workbench_react.useEditor)((s) => s.missingAssetIds);
|
|
1436
1456
|
const selectAsset = (0, _cut_workbench_react.useEditor)((s) => s.selectAsset);
|
|
1437
1457
|
const { importing, progress, importFiles } = useMediaImport();
|
|
1438
1458
|
const [metaAssetId, setMetaAssetId] = (0, react.useState)(null);
|
|
@@ -1505,11 +1525,11 @@ function MediaLibraryPanel({ onAddToTimeline, onRemoveAsset }) {
|
|
|
1505
1525
|
}),
|
|
1506
1526
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1507
1527
|
label: "分辨率",
|
|
1508
|
-
value: formatResolution(metaAsset.width, metaAsset.height)
|
|
1528
|
+
value: require_notify.formatResolution(metaAsset.width, metaAsset.height)
|
|
1509
1529
|
}),
|
|
1510
1530
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1511
1531
|
label: "大小",
|
|
1512
|
-
value: metaAsset.kind === "text" ? "—" : formatBytes(metaAsset.size)
|
|
1532
|
+
value: metaAsset.kind === "text" ? "—" : require_notify.formatBytes(metaAsset.size)
|
|
1513
1533
|
}),
|
|
1514
1534
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1515
1535
|
label: "音轨",
|
|
@@ -1585,6 +1605,7 @@ function CreateTextDialog({ open, onOpenChange, onCreated }) {
|
|
|
1585
1605
|
const projectWidth = (0, _cut_workbench_react.useEditor)((s) => s.project.width);
|
|
1586
1606
|
const projectHeight = (0, _cut_workbench_react.useEditor)((s) => s.project.height);
|
|
1587
1607
|
const addAsset = (0, _cut_workbench_react.useEditor)((s) => s.addAsset);
|
|
1608
|
+
const { bridge } = require_notify.useWorkbench();
|
|
1588
1609
|
const handleCreate = () => {
|
|
1589
1610
|
const { asset } = (0, _cut_workbench_core_engine.createTextAsset)({
|
|
1590
1611
|
registry: editor.runtime.clips,
|
|
@@ -1595,7 +1616,10 @@ function CreateTextDialog({ open, onOpenChange, onCreated }) {
|
|
|
1595
1616
|
addAsset(asset);
|
|
1596
1617
|
onCreated(asset.id);
|
|
1597
1618
|
onOpenChange(false);
|
|
1598
|
-
|
|
1619
|
+
require_notify.notify(bridge, {
|
|
1620
|
+
level: "success",
|
|
1621
|
+
title: "已创建文字素材"
|
|
1622
|
+
});
|
|
1599
1623
|
};
|
|
1600
1624
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dialog, {
|
|
1601
1625
|
open,
|
|
@@ -1629,10 +1653,15 @@ function CreateSubtitleDialog({ open, onOpenChange, onCreated }) {
|
|
|
1629
1653
|
const projectWidth = (0, _cut_workbench_react.useEditor)((s) => s.project.width);
|
|
1630
1654
|
const projectHeight = (0, _cut_workbench_react.useEditor)((s) => s.project.height);
|
|
1631
1655
|
const addAsset = (0, _cut_workbench_react.useEditor)((s) => s.addAsset);
|
|
1656
|
+
const { bridge } = require_notify.useWorkbench();
|
|
1632
1657
|
const handleCreate = () => {
|
|
1633
1658
|
const durationUs = (0, _cut_workbench_core.parseSrtDurationUs)(srt);
|
|
1634
1659
|
if (durationUs <= 0) {
|
|
1635
|
-
|
|
1660
|
+
require_notify.notify(bridge, {
|
|
1661
|
+
level: "error",
|
|
1662
|
+
title: "SRT 解析失败",
|
|
1663
|
+
description: "请检查时间轴格式:00:00:00,000 --> 00:00:02,000"
|
|
1664
|
+
});
|
|
1636
1665
|
return;
|
|
1637
1666
|
}
|
|
1638
1667
|
const { asset } = (0, _cut_workbench_core_engine.createSubtitleAsset)({
|
|
@@ -1645,7 +1674,10 @@ function CreateSubtitleDialog({ open, onOpenChange, onCreated }) {
|
|
|
1645
1674
|
addAsset(asset);
|
|
1646
1675
|
onCreated(asset.id);
|
|
1647
1676
|
onOpenChange(false);
|
|
1648
|
-
|
|
1677
|
+
require_notify.notify(bridge, {
|
|
1678
|
+
level: "success",
|
|
1679
|
+
title: "已创建字幕素材"
|
|
1680
|
+
});
|
|
1649
1681
|
};
|
|
1650
1682
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dialog, {
|
|
1651
1683
|
open,
|
|
@@ -1671,11 +1703,22 @@ function CreateSubtitleDialog({ open, onOpenChange, onCreated }) {
|
|
|
1671
1703
|
});
|
|
1672
1704
|
}
|
|
1673
1705
|
// src/hooks/use-toast-safe.ts
|
|
1674
|
-
/**
|
|
1706
|
+
/**
|
|
1707
|
+
* 把异常转成可见提示,禁止静默吞掉。
|
|
1708
|
+
*
|
|
1709
|
+
* 这是**非 hook** 版本,供事件回调 / 非组件代码调用(例如 `useAvCanvas` 的
|
|
1710
|
+
* 初始化失败)。它拿不到 React context,因此走 `notify` 注册的默认桥:
|
|
1711
|
+
* `WorkbenchProvider` 挂载时会把 `bridge` 注册进来,宿主实现了 `notify`
|
|
1712
|
+
* 就交给宿主,否则退回库内 toast。
|
|
1713
|
+
*/
|
|
1675
1714
|
function reportError(error, fallback = "操作失败") {
|
|
1676
1715
|
const message = error instanceof Error ? error.message : String(error);
|
|
1677
1716
|
console.error(error);
|
|
1678
|
-
|
|
1717
|
+
require_notify.notify(void 0, {
|
|
1718
|
+
level: "error",
|
|
1719
|
+
title: fallback,
|
|
1720
|
+
description: message
|
|
1721
|
+
});
|
|
1679
1722
|
}
|
|
1680
1723
|
// src/features/preview/hooks/useAvCanvas.ts
|
|
1681
1724
|
/**
|
|
@@ -2305,7 +2348,7 @@ function PreviewStage({ attachRef, width, height, emptyHint, onCapture, onFullsc
|
|
|
2305
2348
|
const scaleY = height > 0 ? box.h / height : 1;
|
|
2306
2349
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2307
2350
|
ref: outerRef,
|
|
2308
|
-
className: cn("relative flex min-h-0 flex-1 items-center justify-center overflow-hidden bg-stage-bg", className),
|
|
2351
|
+
className: require_notify.cn("relative flex min-h-0 flex-1 items-center justify-center overflow-hidden bg-stage-bg", className),
|
|
2309
2352
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2310
2353
|
className: "relative shrink-0 overflow-hidden rounded-md bg-black shadow-2xl ring-1 ring-border",
|
|
2311
2354
|
style: {
|
|
@@ -2366,14 +2409,14 @@ function Slider({ className, defaultValue, value, min = 0, max = 100, ...props }
|
|
|
2366
2409
|
value,
|
|
2367
2410
|
min,
|
|
2368
2411
|
max,
|
|
2369
|
-
className: cn("relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col", className),
|
|
2412
|
+
className: require_notify.cn("relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col", className),
|
|
2370
2413
|
...props,
|
|
2371
2414
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Slider.Track, {
|
|
2372
2415
|
"data-slot": "slider-track",
|
|
2373
|
-
className: cn("relative grow overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"),
|
|
2416
|
+
className: require_notify.cn("relative grow overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"),
|
|
2374
2417
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Slider.Range, {
|
|
2375
2418
|
"data-slot": "slider-range",
|
|
2376
|
-
className: cn("absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full")
|
|
2419
|
+
className: require_notify.cn("absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full")
|
|
2377
2420
|
})
|
|
2378
2421
|
}), Array.from({ length: _values.length }, (_, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Slider.Thumb, {
|
|
2379
2422
|
"data-slot": "slider-thumb",
|
|
@@ -2388,7 +2431,7 @@ function Slider({ className, defaultValue, value, min = 0, max = 100, ...props }
|
|
|
2388
2431
|
*/
|
|
2389
2432
|
function Timecode({ us, showMs = true, className, muted, title }) {
|
|
2390
2433
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2391
|
-
className: cn("font-mono text-xs tabular-nums", muted && "text-muted-foreground", className),
|
|
2434
|
+
className: require_notify.cn("font-mono text-xs tabular-nums", muted && "text-muted-foreground", className),
|
|
2392
2435
|
title: title ?? `${us} µs`,
|
|
2393
2436
|
children: (0, _cut_workbench_core.formatTimecode)(us, showMs)
|
|
2394
2437
|
});
|
|
@@ -2400,15 +2443,51 @@ var RATES = [
|
|
|
2400
2443
|
1.5,
|
|
2401
2444
|
2
|
|
2402
2445
|
];
|
|
2446
|
+
/**
|
|
2447
|
+
* 紧凑档位(按容器宽度,不是视口 —— 预览列宽由拖拽决定)。
|
|
2448
|
+
*
|
|
2449
|
+
* 阈值来自实测的控件总宽:完整形态约 680px,`< 700` 先去音量滑杆与速率、
|
|
2450
|
+
* `< 560` 再去时间码的「总时长」与进度条、`< 420` 再去跳首尾。
|
|
2451
|
+
* 只靠 `shrink-0` + `overflow:hidden` 会在 650px 左右把时间码裁掉,
|
|
2452
|
+
* 且**没有任何报错**,只有截图能看出来 —— 所以这里显式分级。
|
|
2453
|
+
*/
|
|
2454
|
+
var FULL_PX = 700;
|
|
2455
|
+
var COMPACT_PX = 620;
|
|
2456
|
+
var TIGHT_PX = 520;
|
|
2457
|
+
var ULTRA_COMPACT_PX = 420;
|
|
2458
|
+
/** 低于此宽度连时间码都放不下:只留播放/逐帧,横向可滚 */
|
|
2459
|
+
var MICRO_PX = 200;
|
|
2403
2460
|
/** 播放控制条:逐帧 / 跳首尾 / 播放 / 时间码 / 速率 / 音量 */
|
|
2404
2461
|
function PlaybackControls({ playing, currentUs, durationUs, masterVolume, playbackRate, disabled, onToggle, onJumpStart, onJumpEnd, onStepFrame, onSeek, onVolumeChange, onRateChange, className }) {
|
|
2462
|
+
const rootRef = (0, react.useRef)(null);
|
|
2463
|
+
const [width, setWidth] = (0, react.useState)(0);
|
|
2464
|
+
/**
|
|
2465
|
+
* 用容器宽度而不是 CSS 媒体查询。
|
|
2466
|
+
*
|
|
2467
|
+
* 预览面板宽度由左侧拖拽决定,和视口宽度无关 —— 用户把两侧拉宽时,
|
|
2468
|
+
* 视口一点没变,`max-[560px]:hidden` 这类媒体查询不会触发,时间码照样被压扁。
|
|
2469
|
+
*/
|
|
2470
|
+
(0, react.useEffect)(() => {
|
|
2471
|
+
const el = rootRef.current;
|
|
2472
|
+
if (el == null) return;
|
|
2473
|
+
const ro = new ResizeObserver(() => setWidth(el.clientWidth));
|
|
2474
|
+
ro.observe(el);
|
|
2475
|
+
setWidth(el.clientWidth);
|
|
2476
|
+
return () => ro.disconnect();
|
|
2477
|
+
}, []);
|
|
2478
|
+
const compact = width > 0 && width < COMPACT_PX;
|
|
2479
|
+
const tight = width > 0 && width < TIGHT_PX;
|
|
2480
|
+
const ultraCompact = width > 0 && width < ULTRA_COMPACT_PX;
|
|
2481
|
+
const micro = width > 0 && width < MICRO_PX;
|
|
2482
|
+
const full = width === 0 || width >= FULL_PX;
|
|
2405
2483
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2406
|
-
|
|
2484
|
+
ref: rootRef,
|
|
2485
|
+
className: require_notify.cn("flex h-11 shrink-0 items-center gap-1.5 overflow-hidden border-t bg-card/60", ultraCompact ? "px-1.5" : "px-3", className),
|
|
2407
2486
|
children: [
|
|
2408
2487
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2409
|
-
className: "flex items-center gap-0.5",
|
|
2488
|
+
className: "flex shrink-0 items-center gap-0.5",
|
|
2410
2489
|
children: [
|
|
2411
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
2490
|
+
!ultraCompact && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
2412
2491
|
asChild: true,
|
|
2413
2492
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
2414
2493
|
variant: "ghost",
|
|
@@ -2450,7 +2529,7 @@ function PlaybackControls({ playing, currentUs, durationUs, masterVolume, playba
|
|
|
2450
2529
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.StepForward, { className: "size-3.5" })
|
|
2451
2530
|
})
|
|
2452
2531
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "下一帧 (→)" })] }),
|
|
2453
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
2532
|
+
!ultraCompact && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
2454
2533
|
asChild: true,
|
|
2455
2534
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
2456
2535
|
variant: "ghost",
|
|
@@ -2464,23 +2543,19 @@ function PlaybackControls({ playing, currentUs, durationUs, masterVolume, playba
|
|
|
2464
2543
|
]
|
|
2465
2544
|
}),
|
|
2466
2545
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2467
|
-
className: "mx-2 flex items-baseline gap-1",
|
|
2468
|
-
children: [
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
us: durationUs,
|
|
2479
|
-
muted: true
|
|
2480
|
-
})
|
|
2481
|
-
]
|
|
2546
|
+
className: require_notify.cn("mx-2 flex shrink-0 items-baseline gap-1 whitespace-nowrap", ultraCompact && "mx-1", micro && "hidden"),
|
|
2547
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timecode, {
|
|
2548
|
+
us: currentUs,
|
|
2549
|
+
className: "text-[13px] font-medium"
|
|
2550
|
+
}), !tight && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2551
|
+
className: "text-xs text-muted-foreground",
|
|
2552
|
+
children: "/"
|
|
2553
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timecode, {
|
|
2554
|
+
us: durationUs,
|
|
2555
|
+
muted: true
|
|
2556
|
+
})] })]
|
|
2482
2557
|
}),
|
|
2483
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2558
|
+
!ultraCompact && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2484
2559
|
type: "range",
|
|
2485
2560
|
className: "mx-1 h-1 min-w-16 flex-1 cursor-pointer appearance-none rounded-full bg-muted accent-primary",
|
|
2486
2561
|
min: 0,
|
|
@@ -2492,14 +2567,14 @@ function PlaybackControls({ playing, currentUs, durationUs, masterVolume, playba
|
|
|
2492
2567
|
"aria-label": "播放进度"
|
|
2493
2568
|
}),
|
|
2494
2569
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2495
|
-
className: "flex items-center gap-1",
|
|
2570
|
+
className: require_notify.cn("flex shrink-0 items-center gap-1", compact && "ml-auto", micro && "hidden"),
|
|
2496
2571
|
children: [
|
|
2497
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2572
|
+
!compact && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2498
2573
|
className: "flex items-center rounded-md border p-0.5",
|
|
2499
2574
|
children: RATES.map((rate) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2500
2575
|
type: "button",
|
|
2501
2576
|
onClick: () => onRateChange(rate),
|
|
2502
|
-
className: cn("rounded px-1.5 py-0.5 text-[11px] tabular-nums transition-colors", playbackRate === rate ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground"),
|
|
2577
|
+
className: require_notify.cn("rounded px-1.5 py-0.5 text-[11px] tabular-nums transition-colors", playbackRate === rate ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground"),
|
|
2503
2578
|
title: rate !== 1 ? "变速预览(不会改变导出结果)" : "正常速度",
|
|
2504
2579
|
children: [rate, "×"]
|
|
2505
2580
|
}, rate))
|
|
@@ -2514,7 +2589,7 @@ function PlaybackControls({ playing, currentUs, durationUs, masterVolume, playba
|
|
|
2514
2589
|
children: masterVolume > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Volume2, { className: "size-3.5" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.VolumeX, { className: "size-3.5" })
|
|
2515
2590
|
})
|
|
2516
2591
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: masterVolume > 0 ? "静音预览" : "取消静音" })] }),
|
|
2517
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Slider, {
|
|
2592
|
+
full && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Slider, {
|
|
2518
2593
|
className: "w-20",
|
|
2519
2594
|
min: 0,
|
|
2520
2595
|
max: 1,
|
|
@@ -2563,6 +2638,7 @@ function PreviewPanel() {
|
|
|
2563
2638
|
const setMasterVolume = (0, _cut_workbench_react.useEditor)((s) => s.setMasterVolume);
|
|
2564
2639
|
const playheadUs = (0, _cut_workbench_react.useEditor)((s) => s.playheadUs);
|
|
2565
2640
|
const selectedClipIds = (0, _cut_workbench_react.useEditor)((s) => s.selectedClipIds);
|
|
2641
|
+
const { bridge } = require_notify.useWorkbench();
|
|
2566
2642
|
const canvas = useAvCanvas({
|
|
2567
2643
|
width: project.width,
|
|
2568
2644
|
height: project.height
|
|
@@ -2573,12 +2649,18 @@ function PreviewPanel() {
|
|
|
2573
2649
|
if (errors.length === 0) return;
|
|
2574
2650
|
const missing = errors.filter((e) => e.message.startsWith("素材文件未载入"));
|
|
2575
2651
|
const other = errors.filter((e) => !e.message.startsWith("素材文件未载入"));
|
|
2576
|
-
if (missing.length > 0)
|
|
2652
|
+
if (missing.length > 0) require_notify.notify(bridge, {
|
|
2653
|
+
level: "warning",
|
|
2654
|
+
title: `${missing.length} 个片段缺少源文件,已跳过预览`,
|
|
2577
2655
|
description: "刷新后素材二进制不会保留,请重新导入相同文件",
|
|
2578
2656
|
duration: 8e3
|
|
2579
2657
|
});
|
|
2580
|
-
if (other.length > 0)
|
|
2581
|
-
|
|
2658
|
+
if (other.length > 0) require_notify.notify(bridge, {
|
|
2659
|
+
level: "error",
|
|
2660
|
+
title: "部分片段无法载入预览",
|
|
2661
|
+
description: Array.from(new Set(other.map((e) => e.message))).slice(0, 3).join("\n")
|
|
2662
|
+
});
|
|
2663
|
+
}, [bridge]);
|
|
2582
2664
|
const clock = usePlaybackClock({
|
|
2583
2665
|
project,
|
|
2584
2666
|
assets,
|
|
@@ -2714,12 +2796,18 @@ function PreviewPanel() {
|
|
|
2714
2796
|
const handleCapture = (0, react.useCallback)(() => {
|
|
2715
2797
|
const dataUrl = canvas.captureImage();
|
|
2716
2798
|
if (dataUrl == null) {
|
|
2717
|
-
|
|
2799
|
+
require_notify.notify(bridge, {
|
|
2800
|
+
level: "error",
|
|
2801
|
+
title: "截图失败:画布尚未就绪"
|
|
2802
|
+
});
|
|
2718
2803
|
return;
|
|
2719
2804
|
}
|
|
2720
2805
|
downloadDataUrl(dataUrl, `frame-${Date.now()}.png`);
|
|
2721
|
-
|
|
2722
|
-
|
|
2806
|
+
require_notify.notify(bridge, {
|
|
2807
|
+
level: "success",
|
|
2808
|
+
title: "已导出当前帧"
|
|
2809
|
+
});
|
|
2810
|
+
}, [bridge, canvas]);
|
|
2723
2811
|
const handleFullscreen = () => {
|
|
2724
2812
|
const el = canvas.containerRef.current?.parentElement;
|
|
2725
2813
|
if (el == null) return;
|
|
@@ -2798,143 +2886,148 @@ var DEFAULT_BITRATE = 8e6;
|
|
|
2798
2886
|
/** 时间轴顶部工具条:分割 / 复制 / 删除 / 吸附开关 / 缩放 / 加轨道 */
|
|
2799
2887
|
function TimelineToolbar({ playheadUs, durationUs, pxPerSecond, selectionCount, magnetEnabled, onToggleMagnet, onSplit, onDuplicate, onRemove, onZoomIn, onZoomOut, onZoomToFit, onAddTrack, canZoomIn, canZoomOut }) {
|
|
2800
2888
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2801
|
-
className: "flex h-9 shrink-0 items-center gap-1 border-b bg-card/60 px-2",
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2889
|
+
className: "thin-scrollbar @container flex h-9 shrink-0 items-center gap-1 overflow-x-auto overflow-y-hidden border-b bg-card/60 px-2",
|
|
2890
|
+
role: "toolbar",
|
|
2891
|
+
"aria-label": "时间轴工具",
|
|
2892
|
+
"data-timeline-toolbar": true,
|
|
2893
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2894
|
+
className: "flex shrink-0 items-center gap-1",
|
|
2895
|
+
children: [
|
|
2896
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
2897
|
+
asChild: true,
|
|
2898
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
2899
|
+
variant: "ghost",
|
|
2900
|
+
size: "icon-sm",
|
|
2901
|
+
onClick: onSplit,
|
|
2902
|
+
"aria-label": "分割",
|
|
2903
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Scissors, { className: "size-3.5" })
|
|
2904
|
+
})
|
|
2905
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "在播放头分割 (S)" })] }),
|
|
2906
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
2907
|
+
asChild: true,
|
|
2908
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
2909
|
+
variant: "ghost",
|
|
2910
|
+
size: "icon-sm",
|
|
2911
|
+
onClick: onDuplicate,
|
|
2912
|
+
disabled: selectionCount === 0,
|
|
2913
|
+
"aria-label": "复制片段",
|
|
2914
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Copy, { className: "size-3.5" })
|
|
2915
|
+
})
|
|
2916
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "复制片段 (Ctrl+D)" })] }),
|
|
2917
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
2918
|
+
asChild: true,
|
|
2919
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
2920
|
+
variant: "ghost",
|
|
2921
|
+
size: "icon-sm",
|
|
2922
|
+
onClick: onRemove,
|
|
2923
|
+
disabled: selectionCount === 0,
|
|
2924
|
+
"aria-label": "删除片段",
|
|
2925
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Trash2, { className: "size-3.5" })
|
|
2926
|
+
})
|
|
2927
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "删除片段 (Delete)" })] }),
|
|
2928
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
|
|
2929
|
+
orientation: "vertical",
|
|
2930
|
+
className: "mx-1 h-4"
|
|
2931
|
+
}),
|
|
2932
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
2933
|
+
asChild: true,
|
|
2934
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
2935
|
+
variant: magnetEnabled ? "secondary" : "ghost",
|
|
2936
|
+
size: "icon-sm",
|
|
2937
|
+
onClick: onToggleMagnet,
|
|
2938
|
+
"aria-label": "吸附",
|
|
2939
|
+
"aria-pressed": magnetEnabled,
|
|
2940
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Magnet, { className: "size-3.5" })
|
|
2941
|
+
})
|
|
2942
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "吸附对齐(阈值 15px)" })] }),
|
|
2943
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
|
|
2944
|
+
orientation: "vertical",
|
|
2945
|
+
className: "mx-1 h-4"
|
|
2946
|
+
}),
|
|
2947
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2948
|
+
className: "flex items-center gap-0.5",
|
|
2949
|
+
children: [
|
|
2950
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
2951
|
+
asChild: true,
|
|
2952
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
2953
|
+
variant: "ghost",
|
|
2954
|
+
size: "icon-sm",
|
|
2955
|
+
onClick: onZoomOut,
|
|
2956
|
+
disabled: !canZoomOut,
|
|
2957
|
+
"aria-label": "缩小",
|
|
2958
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ZoomOut, { className: "size-3.5" })
|
|
2959
|
+
})
|
|
2960
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "缩小 (-)" })] }),
|
|
2961
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2962
|
+
className: "w-11 text-center font-mono text-[10px] text-muted-foreground tabular-nums",
|
|
2963
|
+
children: Math.round(pxPerSecond)
|
|
2964
|
+
}),
|
|
2965
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
2966
|
+
asChild: true,
|
|
2967
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
2968
|
+
variant: "ghost",
|
|
2969
|
+
size: "icon-sm",
|
|
2970
|
+
onClick: onZoomIn,
|
|
2971
|
+
disabled: !canZoomIn,
|
|
2972
|
+
"aria-label": "放大",
|
|
2973
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ZoomIn, { className: "size-3.5" })
|
|
2974
|
+
})
|
|
2975
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "放大 (+)" })] }),
|
|
2976
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
2875
2977
|
variant: "ghost",
|
|
2876
|
-
size: "
|
|
2877
|
-
onClick:
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ZoomIn, { className: "size-3.5" })
|
|
2978
|
+
size: "xs",
|
|
2979
|
+
onClick: onZoomToFit,
|
|
2980
|
+
className: "text-[10px]",
|
|
2981
|
+
children: "适应"
|
|
2881
2982
|
})
|
|
2882
|
-
|
|
2883
|
-
|
|
2983
|
+
]
|
|
2984
|
+
}),
|
|
2985
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
|
|
2986
|
+
orientation: "vertical",
|
|
2987
|
+
className: "mx-1 h-4"
|
|
2988
|
+
}),
|
|
2989
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2990
|
+
className: "flex items-center gap-0.5",
|
|
2991
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
|
|
2884
2992
|
variant: "ghost",
|
|
2885
2993
|
size: "xs",
|
|
2886
|
-
onClick:
|
|
2887
|
-
className: "
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
className: "text-foreground"
|
|
2924
|
-
}),
|
|
2925
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2926
|
-
className: "mx-1",
|
|
2927
|
-
children: "/"
|
|
2928
|
-
}),
|
|
2929
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timecode, {
|
|
2930
|
-
us: durationUs,
|
|
2931
|
-
muted: true
|
|
2932
|
-
})
|
|
2933
|
-
]
|
|
2934
|
-
})
|
|
2935
|
-
]
|
|
2936
|
-
})
|
|
2937
|
-
]
|
|
2994
|
+
onClick: () => onAddTrack("visual"),
|
|
2995
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, { className: "size-3" }), "视频轨"]
|
|
2996
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
|
|
2997
|
+
variant: "ghost",
|
|
2998
|
+
size: "xs",
|
|
2999
|
+
onClick: () => onAddTrack("audio"),
|
|
3000
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, { className: "size-3" }), "音频轨"]
|
|
3001
|
+
})]
|
|
3002
|
+
})
|
|
3003
|
+
]
|
|
3004
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3005
|
+
className: "ml-auto hidden shrink-0 items-center gap-2 pl-2 text-[11px] text-muted-foreground @[720px]:flex",
|
|
3006
|
+
children: [
|
|
3007
|
+
selectionCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3008
|
+
className: "rounded bg-primary/15 px-1.5 py-0.5 text-primary",
|
|
3009
|
+
children: ["已选 ", selectionCount]
|
|
3010
|
+
}),
|
|
3011
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Info, { className: "size-3" }),
|
|
3012
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3013
|
+
className: "tabular-nums whitespace-nowrap",
|
|
3014
|
+
children: [
|
|
3015
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timecode, {
|
|
3016
|
+
us: playheadUs,
|
|
3017
|
+
className: "text-foreground"
|
|
3018
|
+
}),
|
|
3019
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3020
|
+
className: "mx-1",
|
|
3021
|
+
children: "/"
|
|
3022
|
+
}),
|
|
3023
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timecode, {
|
|
3024
|
+
us: durationUs,
|
|
3025
|
+
muted: true
|
|
3026
|
+
})
|
|
3027
|
+
]
|
|
3028
|
+
})
|
|
3029
|
+
]
|
|
3030
|
+
})]
|
|
2938
3031
|
});
|
|
2939
3032
|
}
|
|
2940
3033
|
// src/features/timeline/components/Ruler.tsx
|
|
@@ -2951,14 +3044,14 @@ function Ruler({ pxPerSecond, widthPx, playheadPx, scrollLeftPx, className }) {
|
|
|
2951
3044
|
widthPx
|
|
2952
3045
|
]);
|
|
2953
3046
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2954
|
-
className: cn("relative shrink-0 cursor-ew-resize border-b bg-timeline-bg select-none", className),
|
|
3047
|
+
className: require_notify.cn("relative shrink-0 cursor-ew-resize border-b bg-timeline-bg select-none", className),
|
|
2955
3048
|
style: {
|
|
2956
3049
|
height: 28,
|
|
2957
3050
|
width: widthPx
|
|
2958
3051
|
},
|
|
2959
3052
|
children: [
|
|
2960
3053
|
ticks.map((tick) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2961
|
-
className: cn("absolute bottom-0 w-px", tick.major ? "h-2 bg-muted-foreground/50" : "h-1 bg-muted-foreground/25"),
|
|
3054
|
+
className: require_notify.cn("absolute bottom-0 w-px", tick.major ? "h-2 bg-muted-foreground/50" : "h-1 bg-muted-foreground/25"),
|
|
2962
3055
|
style: { left: tick.px }
|
|
2963
3056
|
}, `${tick.us}-${tick.major ? "M" : "m"}`)),
|
|
2964
3057
|
ticks.filter((t) => t.major).map((tick) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -2986,7 +3079,7 @@ function Ruler({ pxPerSecond, widthPx, playheadPx, scrollLeftPx, className }) {
|
|
|
2986
3079
|
*/
|
|
2987
3080
|
var TrackHeader = (0, react.memo)(function TrackHeader({ track, onToggleMute, onToggleLock, onToggleHidden, onRemove }) {
|
|
2988
3081
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2989
|
-
className: cn("flex items-center gap-1 border-r border-b border-timeline-grid px-2", track.kind === "audio" ? "bg-clip-audio/10" : "bg-clip-video/10", track.hidden && "opacity-60"),
|
|
3082
|
+
className: require_notify.cn("flex items-center gap-1 border-r border-b border-timeline-grid px-2", track.kind === "audio" ? "bg-clip-audio/10" : "bg-clip-video/10", track.hidden && "opacity-60"),
|
|
2990
3083
|
style: { height: 56 },
|
|
2991
3084
|
"data-track-header": true,
|
|
2992
3085
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -3041,7 +3134,7 @@ function IconToggle({ active, onClick, label, icon: Icon }) {
|
|
|
3041
3134
|
onClick,
|
|
3042
3135
|
"aria-label": label,
|
|
3043
3136
|
"aria-pressed": active,
|
|
3044
|
-
className: cn("flex size-[18px] items-center justify-center rounded transition-colors", active ? "bg-primary/20 text-primary" : "text-muted-foreground hover:text-foreground"),
|
|
3137
|
+
className: require_notify.cn("flex size-[18px] items-center justify-center rounded transition-colors", active ? "bg-primary/20 text-primary" : "text-muted-foreground hover:text-foreground"),
|
|
3045
3138
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: "size-3" })
|
|
3046
3139
|
})
|
|
3047
3140
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: label })] });
|
|
@@ -3132,7 +3225,7 @@ var ClipBlock = (0, react.memo)(function ClipBlock({ clip, asset, pxPerSecond, s
|
|
|
3132
3225
|
handlers.beginMove(event, clip);
|
|
3133
3226
|
},
|
|
3134
3227
|
onDoubleClick: () => handlers.seekTo(clip),
|
|
3135
|
-
className: cn("group absolute top-1 bottom-1 overflow-hidden rounded-md border text-left select-none transition-shadow", KIND_BG[kind], selected ? "ring-2 ring-primary ring-offset-1 ring-offset-timeline-bg" : "hover:brightness-110", locked ? "cursor-not-allowed opacity-70" : "cursor-grab active:cursor-grabbing"),
|
|
3228
|
+
className: require_notify.cn("group absolute top-1 bottom-1 overflow-hidden rounded-md border text-left select-none transition-shadow", KIND_BG[kind], selected ? "ring-2 ring-primary ring-offset-1 ring-offset-timeline-bg" : "hover:brightness-110", locked ? "cursor-not-allowed opacity-70" : "cursor-grab active:cursor-grabbing"),
|
|
3136
3229
|
style: {
|
|
3137
3230
|
left: leftPx,
|
|
3138
3231
|
width: widthPx,
|
|
@@ -3211,7 +3304,7 @@ function ContextMenuPortal({ ...props }) {
|
|
|
3211
3304
|
function ContextMenuContent({ className, ...props }) {
|
|
3212
3305
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextMenuPortal, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ContextMenu.Content, {
|
|
3213
3306
|
"data-slot": "context-menu-content",
|
|
3214
|
-
className: cn("z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", className),
|
|
3307
|
+
className: require_notify.cn("z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", className),
|
|
3215
3308
|
...props
|
|
3216
3309
|
}) });
|
|
3217
3310
|
}
|
|
@@ -3220,21 +3313,21 @@ function ContextMenuItem({ className, inset, variant = "default", ...props }) {
|
|
|
3220
3313
|
"data-slot": "context-menu-item",
|
|
3221
3314
|
"data-inset": inset,
|
|
3222
3315
|
"data-variant": variant,
|
|
3223
|
-
className: cn("relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!", className),
|
|
3316
|
+
className: require_notify.cn("relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!", className),
|
|
3224
3317
|
...props
|
|
3225
3318
|
});
|
|
3226
3319
|
}
|
|
3227
3320
|
function ContextMenuSeparator({ className, ...props }) {
|
|
3228
3321
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ContextMenu.Separator, {
|
|
3229
3322
|
"data-slot": "context-menu-separator",
|
|
3230
|
-
className: cn("-mx-1 my-1 h-px bg-border", className),
|
|
3323
|
+
className: require_notify.cn("-mx-1 my-1 h-px bg-border", className),
|
|
3231
3324
|
...props
|
|
3232
3325
|
});
|
|
3233
3326
|
}
|
|
3234
3327
|
function ContextMenuShortcut({ className, ...props }) {
|
|
3235
3328
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3236
3329
|
"data-slot": "context-menu-shortcut",
|
|
3237
|
-
className: cn("ml-auto text-xs tracking-widest text-muted-foreground", className),
|
|
3330
|
+
className: require_notify.cn("ml-auto text-xs tracking-widest text-muted-foreground", className),
|
|
3238
3331
|
...props
|
|
3239
3332
|
});
|
|
3240
3333
|
}
|
|
@@ -3291,14 +3384,14 @@ function ClipContextMenu({ children, onSplit, onDuplicate, onRemove, onShowInfo,
|
|
|
3291
3384
|
/** 播放头:一条竖线 + 顶部把手,跨过所有轨道 */
|
|
3292
3385
|
function Playhead({ px, playing, className }) {
|
|
3293
3386
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3294
|
-
className: cn("pointer-events-none absolute top-0 bottom-0 z-20 w-px", className),
|
|
3387
|
+
className: require_notify.cn("pointer-events-none absolute top-0 bottom-0 z-20 w-px", className),
|
|
3295
3388
|
style: { left: px },
|
|
3296
3389
|
"aria-hidden": true,
|
|
3297
3390
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3298
|
-
className: cn("absolute inset-y-0 -left-px w-0.5 bg-playhead", playing && "shadow-[0_0_6px_var(--cw-playhead)]"),
|
|
3391
|
+
className: require_notify.cn("absolute inset-y-0 -left-px w-0.5 bg-playhead", playing && "shadow-[0_0_6px_var(--cw-playhead)]"),
|
|
3299
3392
|
style: { top: 24 }
|
|
3300
3393
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3301
|
-
className: cn("absolute -translate-x-1/2 rounded-sm bg-playhead", playing ? "size-3 rotate-45" : "h-2.5 w-2.5 rotate-45"),
|
|
3394
|
+
className: require_notify.cn("absolute -translate-x-1/2 rounded-sm bg-playhead", playing ? "size-3 rotate-45" : "h-2.5 w-2.5 rotate-45"),
|
|
3302
3395
|
style: { top: 14 }
|
|
3303
3396
|
})]
|
|
3304
3397
|
});
|
|
@@ -3307,7 +3400,7 @@ function Playhead({ px, playing, className }) {
|
|
|
3307
3400
|
/** 框选矩形 */
|
|
3308
3401
|
function Marquee({ x1, y1, x2, y2, className }) {
|
|
3309
3402
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3310
|
-
className: cn("pointer-events-none absolute z-40 rounded-sm border border-primary/70 bg-primary/15", className),
|
|
3403
|
+
className: require_notify.cn("pointer-events-none absolute z-40 rounded-sm border border-primary/70 bg-primary/15", className),
|
|
3311
3404
|
style: {
|
|
3312
3405
|
left: Math.min(x1, x2),
|
|
3313
3406
|
top: Math.min(y1, y2),
|
|
@@ -3325,10 +3418,10 @@ function Marquee({ x1, y1, x2, y2, className }) {
|
|
|
3325
3418
|
* 调整回去,视觉上内容不会乱跳。
|
|
3326
3419
|
*/
|
|
3327
3420
|
function useTimelineZoom(containerRef) {
|
|
3328
|
-
const uiApi = useUiStoreApi();
|
|
3329
|
-
const pxPerSecond = useUi((s) => s.pxPerSecond);
|
|
3330
|
-
const setPxPerSecond = useUi((s) => s.setPxPerSecond);
|
|
3331
|
-
const setScrollLeftPx = useUi((s) => s.setScrollLeftPx);
|
|
3421
|
+
const uiApi = require_notify.useUiStoreApi();
|
|
3422
|
+
const pxPerSecond = require_notify.useUi((s) => s.pxPerSecond);
|
|
3423
|
+
const setPxPerSecond = require_notify.useUi((s) => s.setPxPerSecond);
|
|
3424
|
+
const setScrollLeftPx = require_notify.useUi((s) => s.setScrollLeftPx);
|
|
3332
3425
|
const applyZoom = (0, react.useCallback)((nextPps, anchorClientX) => {
|
|
3333
3426
|
const container = containerRef.current;
|
|
3334
3427
|
const prevPps = uiApi.getState().pxPerSecond;
|
|
@@ -3653,7 +3746,7 @@ function useTimelineDrag({ pxPerSecond, contentLeftPx, resolveTrackAt, snapEnabl
|
|
|
3653
3746
|
*/
|
|
3654
3747
|
function Timeline() {
|
|
3655
3748
|
const editor = (0, _cut_workbench_react.useCutEditor)();
|
|
3656
|
-
const uiApi = useUiStoreApi();
|
|
3749
|
+
const uiApi = require_notify.useUiStoreApi();
|
|
3657
3750
|
const project = (0, _cut_workbench_react.useEditor)((s) => s.project);
|
|
3658
3751
|
const assets = (0, _cut_workbench_react.useEditor)((s) => s.assets);
|
|
3659
3752
|
const selectedClipIds = (0, _cut_workbench_react.useEditor)((s) => s.selectedClipIds);
|
|
@@ -3898,7 +3991,7 @@ function Timeline() {
|
|
|
3898
3991
|
splitClip
|
|
3899
3992
|
]);
|
|
3900
3993
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3901
|
-
className: "flex min-h-0 flex-1 flex-col",
|
|
3994
|
+
className: "flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden",
|
|
3902
3995
|
children: [
|
|
3903
3996
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimelineToolbar, {
|
|
3904
3997
|
playheadUs,
|
|
@@ -3918,7 +4011,7 @@ function Timeline() {
|
|
|
3918
4011
|
canZoomOut: zoom.canZoomOut
|
|
3919
4012
|
}),
|
|
3920
4013
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3921
|
-
className: "relative flex min-h-0 flex-1",
|
|
4014
|
+
className: "relative flex min-h-0 min-w-0 flex-1 overflow-hidden",
|
|
3922
4015
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3923
4016
|
className: "thin-scrollbar flex shrink-0 flex-col overflow-y-auto border-r",
|
|
3924
4017
|
style: { width: 150 },
|
|
@@ -3982,7 +4075,7 @@ function Timeline() {
|
|
|
3982
4075
|
"data-timeline-tracks": true,
|
|
3983
4076
|
children: [
|
|
3984
4077
|
project.tracks.map((track, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3985
|
-
className: cn("relative border-b border-timeline-grid", track.kind === "audio" ? "bg-timeline-track/30" : "bg-timeline-track/50", track.hidden && "opacity-40"),
|
|
4078
|
+
className: require_notify.cn("relative border-b border-timeline-grid", track.kind === "audio" ? "bg-timeline-track/30" : "bg-timeline-track/50", track.hidden && "opacity-40"),
|
|
3986
4079
|
style: { height: 56 },
|
|
3987
4080
|
"data-track-row": track.id,
|
|
3988
4081
|
children: [
|
|
@@ -4053,7 +4146,7 @@ function Tabs({ className, orientation = "horizontal", ...props }) {
|
|
|
4053
4146
|
"data-slot": "tabs",
|
|
4054
4147
|
"data-orientation": orientation,
|
|
4055
4148
|
orientation,
|
|
4056
|
-
className: cn("group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", className),
|
|
4149
|
+
className: require_notify.cn("group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", className),
|
|
4057
4150
|
...props
|
|
4058
4151
|
});
|
|
4059
4152
|
}
|
|
@@ -4068,21 +4161,21 @@ function TabsList({ className, variant = "default", ...props }) {
|
|
|
4068
4161
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Tabs.List, {
|
|
4069
4162
|
"data-slot": "tabs-list",
|
|
4070
4163
|
"data-variant": variant,
|
|
4071
|
-
className: cn(tabsListVariants({ variant }), className),
|
|
4164
|
+
className: require_notify.cn(tabsListVariants({ variant }), className),
|
|
4072
4165
|
...props
|
|
4073
4166
|
});
|
|
4074
4167
|
}
|
|
4075
4168
|
function TabsTrigger({ className, ...props }) {
|
|
4076
4169
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Tabs.Trigger, {
|
|
4077
4170
|
"data-slot": "tabs-trigger",
|
|
4078
|
-
className: cn("relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent", "data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground", "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100", className),
|
|
4171
|
+
className: require_notify.cn("relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent", "data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground", "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100", className),
|
|
4079
4172
|
...props
|
|
4080
4173
|
});
|
|
4081
4174
|
}
|
|
4082
4175
|
function TabsContent({ className, ...props }) {
|
|
4083
4176
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Tabs.Content, {
|
|
4084
4177
|
"data-slot": "tabs-content",
|
|
4085
|
-
className: cn("flex-1 outline-none", className),
|
|
4178
|
+
className: require_notify.cn("flex-1 outline-none", className),
|
|
4086
4179
|
...props
|
|
4087
4180
|
});
|
|
4088
4181
|
}
|
|
@@ -4091,7 +4184,7 @@ function Input({ className, type, ...props }) {
|
|
|
4091
4184
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4092
4185
|
type,
|
|
4093
4186
|
"data-slot": "input",
|
|
4094
|
-
className: cn("h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30", "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40", className),
|
|
4187
|
+
className: require_notify.cn("h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30", "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40", className),
|
|
4095
4188
|
...props
|
|
4096
4189
|
});
|
|
4097
4190
|
}
|
|
@@ -4112,7 +4205,7 @@ function SelectTrigger({ className, size = "default", children, ...props }) {
|
|
|
4112
4205
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Select.Trigger, {
|
|
4113
4206
|
"data-slot": "select-trigger",
|
|
4114
4207
|
"data-size": size,
|
|
4115
|
-
className: cn("flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground", className),
|
|
4208
|
+
className: require_notify.cn("flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground", className),
|
|
4116
4209
|
...props,
|
|
4117
4210
|
children: [children, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Select.Icon, {
|
|
4118
4211
|
asChild: true,
|
|
@@ -4126,14 +4219,14 @@ function SelectContent({ className, children, position = "item-aligned", align =
|
|
|
4126
4219
|
container: container ?? void 0,
|
|
4127
4220
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Select.Content, {
|
|
4128
4221
|
"data-slot": "select-content",
|
|
4129
|
-
className: cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className),
|
|
4222
|
+
className: require_notify.cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className),
|
|
4130
4223
|
position,
|
|
4131
4224
|
align,
|
|
4132
4225
|
...props,
|
|
4133
4226
|
children: [
|
|
4134
4227
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectScrollUpButton, {}),
|
|
4135
4228
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Select.Viewport, {
|
|
4136
|
-
className: cn("p-1", position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"),
|
|
4229
|
+
className: require_notify.cn("p-1", position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"),
|
|
4137
4230
|
children
|
|
4138
4231
|
}),
|
|
4139
4232
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectScrollDownButton, {})
|
|
@@ -4144,7 +4237,7 @@ function SelectContent({ className, children, position = "item-aligned", align =
|
|
|
4144
4237
|
function SelectItem({ className, children, ...props }) {
|
|
4145
4238
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Select.Item, {
|
|
4146
4239
|
"data-slot": "select-item",
|
|
4147
|
-
className: cn("relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", className),
|
|
4240
|
+
className: require_notify.cn("relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", className),
|
|
4148
4241
|
...props,
|
|
4149
4242
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4150
4243
|
"data-slot": "select-item-indicator",
|
|
@@ -4156,7 +4249,7 @@ function SelectItem({ className, children, ...props }) {
|
|
|
4156
4249
|
function SelectScrollUpButton({ className, ...props }) {
|
|
4157
4250
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Select.ScrollUpButton, {
|
|
4158
4251
|
"data-slot": "select-scroll-up-button",
|
|
4159
|
-
className: cn("flex cursor-default items-center justify-center py-1", className),
|
|
4252
|
+
className: require_notify.cn("flex cursor-default items-center justify-center py-1", className),
|
|
4160
4253
|
...props,
|
|
4161
4254
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronUpIcon, { className: "size-4" })
|
|
4162
4255
|
});
|
|
@@ -4164,7 +4257,7 @@ function SelectScrollUpButton({ className, ...props }) {
|
|
|
4164
4257
|
function SelectScrollDownButton({ className, ...props }) {
|
|
4165
4258
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Select.ScrollDownButton, {
|
|
4166
4259
|
"data-slot": "select-scroll-down-button",
|
|
4167
|
-
className: cn("flex cursor-default items-center justify-center py-1", className),
|
|
4260
|
+
className: require_notify.cn("flex cursor-default items-center justify-center py-1", className),
|
|
4168
4261
|
...props,
|
|
4169
4262
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDownIcon, { className: "size-4" })
|
|
4170
4263
|
});
|
|
@@ -4173,7 +4266,7 @@ function SelectScrollDownButton({ className, ...props }) {
|
|
|
4173
4266
|
/** 属性面板的一行:标签 + 控件(标签在左,控件占满剩余宽度) */
|
|
4174
4267
|
function Field({ label, hint, children, className, htmlFor }) {
|
|
4175
4268
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4176
|
-
className: cn("flex items-center gap-2", className),
|
|
4269
|
+
className: require_notify.cn("flex items-center gap-2", className),
|
|
4177
4270
|
children: [
|
|
4178
4271
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Label, {
|
|
4179
4272
|
htmlFor,
|
|
@@ -4194,7 +4287,7 @@ function Field({ label, hint, children, className, htmlFor }) {
|
|
|
4194
4287
|
/** 属性分组 */
|
|
4195
4288
|
function Section({ title, children, action, className }) {
|
|
4196
4289
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
4197
|
-
className: cn("space-y-2", className),
|
|
4290
|
+
className: require_notify.cn("space-y-2", className),
|
|
4198
4291
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4199
4292
|
className: "flex items-center justify-between",
|
|
4200
4293
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
@@ -4389,7 +4482,7 @@ function VideoProps({ clip, onPatch, onPatchTransform, onPatchSize }) {
|
|
|
4389
4482
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
|
|
4390
4483
|
type: "number",
|
|
4391
4484
|
className: "h-7 text-xs tabular-nums",
|
|
4392
|
-
value: formatSeconds(clip.startUs),
|
|
4485
|
+
value: require_notify.formatSeconds(clip.startUs),
|
|
4393
4486
|
step: .1,
|
|
4394
4487
|
min: 0,
|
|
4395
4488
|
onChange: (e) => {
|
|
@@ -4403,7 +4496,7 @@ function VideoProps({ clip, onPatch, onPatchTransform, onPatchSize }) {
|
|
|
4403
4496
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
|
|
4404
4497
|
type: "number",
|
|
4405
4498
|
className: "h-7 text-xs tabular-nums",
|
|
4406
|
-
value: formatSeconds(clip.durationUs),
|
|
4499
|
+
value: require_notify.formatSeconds(clip.durationUs),
|
|
4407
4500
|
step: .1,
|
|
4408
4501
|
min: .1,
|
|
4409
4502
|
onChange: (e) => {
|
|
@@ -4510,7 +4603,7 @@ function AudioProps({ clip, onPatch }) {
|
|
|
4510
4603
|
className: "text-[10px] text-muted-foreground",
|
|
4511
4604
|
children: [
|
|
4512
4605
|
"片段时长 ",
|
|
4513
|
-
formatSeconds(clip.durationUs),
|
|
4606
|
+
require_notify.formatSeconds(clip.durationUs),
|
|
4514
4607
|
"s,淡入使用 setAnimation 线性插值。"
|
|
4515
4608
|
]
|
|
4516
4609
|
})
|
|
@@ -4765,7 +4858,7 @@ function SubtitleProps({ clip, onPatch, onRebuild }) {
|
|
|
4765
4858
|
className: "text-[10px] text-muted-foreground",
|
|
4766
4859
|
children: [
|
|
4767
4860
|
"片段时长 ",
|
|
4768
|
-
formatSeconds(clip.durationUs),
|
|
4861
|
+
require_notify.formatSeconds(clip.durationUs),
|
|
4769
4862
|
"s",
|
|
4770
4863
|
cues.length > 0 && ` · 最后一条结束于 ${(cues[cues.length - 1].endMs / 1e3).toFixed(2)}s`
|
|
4771
4864
|
]
|
|
@@ -5037,7 +5130,7 @@ function InspectorPanel() {
|
|
|
5037
5130
|
title: asset.name,
|
|
5038
5131
|
children: asset.name
|
|
5039
5132
|
}),
|
|
5040
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
|
|
5133
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_notify.Badge, {
|
|
5041
5134
|
variant: "outline",
|
|
5042
5135
|
className: "text-[10px]",
|
|
5043
5136
|
children: KIND_LABEL[asset.kind]
|
|
@@ -5060,7 +5153,7 @@ function InspectorPanel() {
|
|
|
5060
5153
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
5061
5154
|
(0, _cut_workbench_core.formatDuration)(clip.durationUs),
|
|
5062
5155
|
" · ",
|
|
5063
|
-
formatResolution(asset.width, asset.height)
|
|
5156
|
+
require_notify.formatResolution(asset.width, asset.height)
|
|
5064
5157
|
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
5065
5158
|
"入点 ",
|
|
5066
5159
|
(clip.inPointUs / 1e6).toFixed(2),
|
|
@@ -5165,11 +5258,11 @@ function Switch({ className, size = "default", ...props }) {
|
|
|
5165
5258
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Switch.Root, {
|
|
5166
5259
|
"data-slot": "switch",
|
|
5167
5260
|
"data-size": size,
|
|
5168
|
-
className: cn("peer group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80", className),
|
|
5261
|
+
className: require_notify.cn("peer group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80", className),
|
|
5169
5262
|
...props,
|
|
5170
5263
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Switch.Thumb, {
|
|
5171
5264
|
"data-slot": "switch-thumb",
|
|
5172
|
-
className: cn("pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground")
|
|
5265
|
+
className: require_notify.cn("pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground")
|
|
5173
5266
|
})
|
|
5174
5267
|
});
|
|
5175
5268
|
}
|
|
@@ -5177,7 +5270,7 @@ function Switch({ className, size = "default", ...props }) {
|
|
|
5177
5270
|
function Progress({ className, value, ...props }) {
|
|
5178
5271
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Progress.Root, {
|
|
5179
5272
|
"data-slot": "progress",
|
|
5180
|
-
className: cn("relative h-2 w-full overflow-hidden rounded-full bg-primary/20", className),
|
|
5273
|
+
className: require_notify.cn("relative h-2 w-full overflow-hidden rounded-full bg-primary/20", className),
|
|
5181
5274
|
...props,
|
|
5182
5275
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Progress.Indicator, {
|
|
5183
5276
|
"data-slot": "progress-indicator",
|
|
@@ -5708,8 +5801,8 @@ var FPS_OPTIONS = [
|
|
|
5708
5801
|
* 所有重活都在 Worker 里;这里只负责 UI 与状态机。
|
|
5709
5802
|
*/
|
|
5710
5803
|
function ExportDialog() {
|
|
5711
|
-
const open = useUi((s) => s.dialog === "export");
|
|
5712
|
-
const closeDialog = useUi((s) => s.closeDialog);
|
|
5804
|
+
const open = require_notify.useUi((s) => s.dialog === "export");
|
|
5805
|
+
const closeDialog = require_notify.useUi((s) => s.closeDialog);
|
|
5713
5806
|
const project = (0, _cut_workbench_react.useEditor)((s) => s.project);
|
|
5714
5807
|
const assets = (0, _cut_workbench_react.useEditor)((s) => s.assets);
|
|
5715
5808
|
const [preset, setPreset] = (0, react.useState)("1080p");
|
|
@@ -5855,7 +5948,7 @@ function ExportDialog() {
|
|
|
5855
5948
|
className: "flex justify-between",
|
|
5856
5949
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "预估大小" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5857
5950
|
className: "font-mono tabular-nums",
|
|
5858
|
-
children: formatBytes(estimatedBytes)
|
|
5951
|
+
children: require_notify.formatBytes(estimatedBytes)
|
|
5859
5952
|
})]
|
|
5860
5953
|
}),
|
|
5861
5954
|
hasTextClips && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -5892,10 +5985,10 @@ function ExportDialog() {
|
|
|
5892
5985
|
children: [
|
|
5893
5986
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.CheckCircle2, { className: "size-3.5" }),
|
|
5894
5987
|
"导出完成 · 耗时 ",
|
|
5895
|
-
formatElapsed(state.result.elapsedMs / 1e3),
|
|
5988
|
+
require_notify.formatElapsed(state.result.elapsedMs / 1e3),
|
|
5896
5989
|
" ·",
|
|
5897
5990
|
" ",
|
|
5898
|
-
formatBytes(state.result.blob.size),
|
|
5991
|
+
require_notify.formatBytes(state.result.blob.size),
|
|
5899
5992
|
state.result.mode === "main" && "(主线程)"
|
|
5900
5993
|
]
|
|
5901
5994
|
}),
|
|
@@ -5958,7 +6051,7 @@ function ExportProgress({ progress, label = "正在合成…", className }) {
|
|
|
5958
6051
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Loader2, { className: "size-3 animate-spin" }), label]
|
|
5959
6052
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5960
6053
|
className: "font-mono tabular-nums",
|
|
5961
|
-
children: formatPercent(progress)
|
|
6054
|
+
children: require_notify.formatPercent(progress)
|
|
5962
6055
|
})]
|
|
5963
6056
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Progress, { value: progress * 100 })]
|
|
5964
6057
|
});
|
|
@@ -6034,7 +6127,7 @@ function CanvasSettingsBody({ onDone }) {
|
|
|
6034
6127
|
setDraftW(String(preset.width));
|
|
6035
6128
|
setDraftH(String(preset.height));
|
|
6036
6129
|
},
|
|
6037
|
-
className: cn("flex flex-col items-center gap-1 rounded-md border px-2 py-2 text-[11px] transition-colors", activePreset?.id === preset.id ? "border-primary bg-primary/10 text-foreground" : "text-muted-foreground hover:border-primary/50 hover:text-foreground"),
|
|
6130
|
+
className: require_notify.cn("flex flex-col items-center gap-1 rounded-md border px-2 py-2 text-[11px] transition-colors", activePreset?.id === preset.id ? "border-primary bg-primary/10 text-foreground" : "text-muted-foreground hover:border-primary/50 hover:text-foreground"),
|
|
6038
6131
|
title: preset.hint,
|
|
6039
6132
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6040
6133
|
className: "block rounded-[2px] border border-current opacity-70",
|
|
@@ -6095,7 +6188,7 @@ function CanvasSettingsBody({ onDone }) {
|
|
|
6095
6188
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6096
6189
|
type: "button",
|
|
6097
6190
|
onClick: () => setMode("scale"),
|
|
6098
|
-
className: cn("rounded-md border px-2.5 py-2 text-left text-[11px] transition-colors", mode === "scale" ? "border-primary bg-primary/10" : "text-muted-foreground hover:border-primary/50"),
|
|
6191
|
+
className: require_notify.cn("rounded-md border px-2.5 py-2 text-left text-[11px] transition-colors", mode === "scale" ? "border-primary bg-primary/10" : "text-muted-foreground hover:border-primary/50"),
|
|
6099
6192
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6100
6193
|
className: "block font-medium text-foreground",
|
|
6101
6194
|
children: "随画布缩放"
|
|
@@ -6106,7 +6199,7 @@ function CanvasSettingsBody({ onDone }) {
|
|
|
6106
6199
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6107
6200
|
type: "button",
|
|
6108
6201
|
onClick: () => setMode("keep"),
|
|
6109
|
-
className: cn("rounded-md border px-2.5 py-2 text-left text-[11px] transition-colors", mode === "keep" ? "border-primary bg-primary/10" : "text-muted-foreground hover:border-primary/50"),
|
|
6202
|
+
className: require_notify.cn("rounded-md border px-2.5 py-2 text-left text-[11px] transition-colors", mode === "keep" ? "border-primary bg-primary/10" : "text-muted-foreground hover:border-primary/50"),
|
|
6110
6203
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6111
6204
|
className: "block font-medium text-foreground",
|
|
6112
6205
|
children: "保持原位置"
|
|
@@ -6168,16 +6261,23 @@ function useAddToTimeline() {
|
|
|
6168
6261
|
const assets = (0, _cut_workbench_react.useEditor)((s) => s.assets);
|
|
6169
6262
|
const playheadUs = (0, _cut_workbench_react.useEditor)((s) => s.playheadUs);
|
|
6170
6263
|
const addClip = (0, _cut_workbench_react.useEditor)((s) => s.addClip);
|
|
6264
|
+
const { bridge } = require_notify.useWorkbench();
|
|
6171
6265
|
return (0, react.useCallback)((assetId) => {
|
|
6172
6266
|
const asset = assets.find((a) => a.id === assetId);
|
|
6173
6267
|
if (!asset) {
|
|
6174
|
-
|
|
6268
|
+
require_notify.notify(bridge, {
|
|
6269
|
+
level: "error",
|
|
6270
|
+
title: "素材不存在,可能已被删除"
|
|
6271
|
+
});
|
|
6175
6272
|
return;
|
|
6176
6273
|
}
|
|
6177
6274
|
const kindIsAudio = asset.kind === "audio";
|
|
6178
6275
|
const track = project.tracks.find((t) => t.kind === (kindIsAudio ? "audio" : "visual")) ?? project.tracks[0];
|
|
6179
6276
|
if (!track) {
|
|
6180
|
-
|
|
6277
|
+
require_notify.notify(bridge, {
|
|
6278
|
+
level: "error",
|
|
6279
|
+
title: "没有可用轨道"
|
|
6280
|
+
});
|
|
6181
6281
|
return;
|
|
6182
6282
|
}
|
|
6183
6283
|
const trackEnd = (0, _cut_workbench_core.getTrackEnd)(project, track.id);
|
|
@@ -6210,6 +6310,7 @@ function useAddToTimeline() {
|
|
|
6210
6310
|
}, [
|
|
6211
6311
|
addClip,
|
|
6212
6312
|
assets,
|
|
6313
|
+
bridge,
|
|
6213
6314
|
playheadUs,
|
|
6214
6315
|
project
|
|
6215
6316
|
]);
|
|
@@ -6246,1340 +6347,173 @@ var FRAME_US = 1e6 / 30;
|
|
|
6246
6347
|
* 全局快捷键。
|
|
6247
6348
|
*
|
|
6248
6349
|
* | 键 | 行为 |
|
|
6249
|
-
* |---|---|
|
|
6250
|
-
* | Space | 播放 / 暂停 |
|
|
6251
|
-
* | S | 在播放头分割选中片段(未选中则分割命中的全部片段) |
|
|
6252
|
-
* | Delete / Backspace | 删除选中片段 |
|
|
6253
|
-
* | Ctrl+Z / Ctrl+Shift+Z | 撤销 / 重做 |
|
|
6254
|
-
* | Ctrl+D | 复制选中片段 |
|
|
6255
|
-
* | Ctrl+A | 全选 |
|
|
6256
|
-
* | ← / → | 逐帧(1/30 s) |
|
|
6257
|
-
* | Home / End | 跳首 / 跳尾 |
|
|
6258
|
-
* | + / - | 时间轴缩放 |
|
|
6259
|
-
* | Esc | 清空选择 |
|
|
6260
|
-
*
|
|
6261
|
-
* 设计:编辑类操作直接调 store;播放类操作通过 `cut-test:*` 自定义事件通知预览面板,
|
|
6262
|
-
* 避免 store → features 的反向依赖。
|
|
6263
|
-
*/
|
|
6264
|
-
function useKeyboardShortcuts() {
|
|
6265
|
-
const editor = (0, _cut_workbench_react.useCutEditor)();
|
|
6266
|
-
const uiApi = useUiStoreApi();
|
|
6267
|
-
const handler = (0, react.useCallback)((event) => {
|
|
6268
|
-
const target = event.target;
|
|
6269
|
-
const isEditable = target != null && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable || target.getAttribute("role") === "textbox" || target.closest("[role=menu]") != null);
|
|
6270
|
-
const mod = event.ctrlKey || event.metaKey;
|
|
6271
|
-
if (mod && event.key.toLowerCase() === "z") {
|
|
6272
|
-
event.preventDefault();
|
|
6273
|
-
if (event.shiftKey) editor.store.getState().redo();
|
|
6274
|
-
else editor.store.getState().undo();
|
|
6275
|
-
return;
|
|
6276
|
-
}
|
|
6277
|
-
if (mod && event.key.toLowerCase() === "d") {
|
|
6278
|
-
event.preventDefault();
|
|
6279
|
-
editor.store.getState().duplicateSelection(5e5);
|
|
6280
|
-
return;
|
|
6281
|
-
}
|
|
6282
|
-
if (mod && event.key.toLowerCase() === "a" && !isEditable) {
|
|
6283
|
-
event.preventDefault();
|
|
6284
|
-
const state = editor.store.getState();
|
|
6285
|
-
state.selectClips(state.project.clips.map((c) => c.id));
|
|
6286
|
-
return;
|
|
6287
|
-
}
|
|
6288
|
-
if (mod && (event.key === "+" || event.key === "=" || event.key === "-" || event.key === "_")) {
|
|
6289
|
-
event.preventDefault();
|
|
6290
|
-
const ui = uiApi.getState();
|
|
6291
|
-
const factor = event.key === "-" || event.key === "_" ? 1 / 1.25 : 1.25;
|
|
6292
|
-
ui.setPxPerSecond((0, _cut_workbench_core.clamp)(ui.pxPerSecond * factor, 10, 500));
|
|
6293
|
-
return;
|
|
6294
|
-
}
|
|
6295
|
-
if (isEditable || mod) return;
|
|
6296
|
-
switch (event.key) {
|
|
6297
|
-
case " ":
|
|
6298
|
-
event.preventDefault();
|
|
6299
|
-
window.dispatchEvent(new CustomEvent("cut-test:toggle-play"));
|
|
6300
|
-
break;
|
|
6301
|
-
case "s":
|
|
6302
|
-
case "S": {
|
|
6303
|
-
event.preventDefault();
|
|
6304
|
-
const state = editor.store.getState();
|
|
6305
|
-
state.splitClipsAt(state.playheadUs, state.selectedClipIds.length > 0 ? state.selectedClipIds : void 0);
|
|
6306
|
-
break;
|
|
6307
|
-
}
|
|
6308
|
-
case "Delete":
|
|
6309
|
-
case "Backspace": {
|
|
6310
|
-
const state = editor.store.getState();
|
|
6311
|
-
if (state.selectedClipIds.length === 0) return;
|
|
6312
|
-
event.preventDefault();
|
|
6313
|
-
state.removeClips(state.selectedClipIds);
|
|
6314
|
-
break;
|
|
6315
|
-
}
|
|
6316
|
-
case "ArrowLeft":
|
|
6317
|
-
case "ArrowRight": {
|
|
6318
|
-
event.preventDefault();
|
|
6319
|
-
const direction = event.key === "ArrowLeft" ? -1 : 1;
|
|
6320
|
-
const state = editor.store.getState();
|
|
6321
|
-
const total = state.project.clips.reduce((max, c) => Math.max(max, c.startUs + c.durationUs), 0);
|
|
6322
|
-
const next = Math.max(0, Math.min(total, state.playheadUs + direction * FRAME_US));
|
|
6323
|
-
state.setPlayhead(next);
|
|
6324
|
-
window.dispatchEvent(new CustomEvent("cut-test:step-frame", { detail: { direction } }));
|
|
6325
|
-
break;
|
|
6326
|
-
}
|
|
6327
|
-
case "Home":
|
|
6328
|
-
event.preventDefault();
|
|
6329
|
-
editor.store.getState().setPlayhead(0);
|
|
6330
|
-
window.dispatchEvent(new CustomEvent("cut-test:seek", { detail: { us: 0 } }));
|
|
6331
|
-
break;
|
|
6332
|
-
case "End": {
|
|
6333
|
-
event.preventDefault();
|
|
6334
|
-
const state = editor.store.getState();
|
|
6335
|
-
const total = state.project.clips.reduce((max, c) => Math.max(max, c.startUs + c.durationUs), 0);
|
|
6336
|
-
state.setPlayhead(total);
|
|
6337
|
-
window.dispatchEvent(new CustomEvent("cut-test:seek", { detail: { us: total } }));
|
|
6338
|
-
break;
|
|
6339
|
-
}
|
|
6340
|
-
case "+":
|
|
6341
|
-
case "=": {
|
|
6342
|
-
event.preventDefault();
|
|
6343
|
-
const ui = uiApi.getState();
|
|
6344
|
-
ui.setPxPerSecond((0, _cut_workbench_core.clamp)(ui.pxPerSecond * 1.25, 10, 500));
|
|
6345
|
-
break;
|
|
6346
|
-
}
|
|
6347
|
-
case "-":
|
|
6348
|
-
case "_": {
|
|
6349
|
-
event.preventDefault();
|
|
6350
|
-
const ui = uiApi.getState();
|
|
6351
|
-
ui.setPxPerSecond((0, _cut_workbench_core.clamp)(ui.pxPerSecond / 1.25, 10, 500));
|
|
6352
|
-
break;
|
|
6353
|
-
}
|
|
6354
|
-
case "Escape": editor.store.getState().selectClips([]);
|
|
6355
|
-
}
|
|
6356
|
-
}, []);
|
|
6357
|
-
(0, react.useEffect)(() => {
|
|
6358
|
-
window.addEventListener("keydown", handler);
|
|
6359
|
-
return () => window.removeEventListener("keydown", handler);
|
|
6360
|
-
}, [handler]);
|
|
6361
|
-
}
|
|
6362
|
-
// src/hooks/use-capability.ts
|
|
6363
|
-
var cached = null;
|
|
6364
|
-
/**
|
|
6365
|
-
* 启动时探测一次浏览器能力(结果在会话内缓存)。
|
|
6366
|
-
*
|
|
6367
|
-
* 必须在渲染任何 WebAV 组件前完成,否则 opfs-tools 的模块副作用可能直接抛错。
|
|
6368
|
-
*/
|
|
6369
|
-
function useCapability() {
|
|
6370
|
-
const [state, setState] = (0, react.useState)({
|
|
6371
|
-
report: cached,
|
|
6372
|
-
loading: cached == null,
|
|
6373
|
-
error: null
|
|
6374
|
-
});
|
|
6375
|
-
(0, react.useEffect)(() => {
|
|
6376
|
-
if (cached != null) return;
|
|
6377
|
-
let cancelled = false;
|
|
6378
|
-
(0, _cut_workbench_core_engine.detectEnvironment)().then((report) => {
|
|
6379
|
-
cached = report;
|
|
6380
|
-
if (!cancelled) setState({
|
|
6381
|
-
report,
|
|
6382
|
-
loading: false,
|
|
6383
|
-
error: null
|
|
6384
|
-
});
|
|
6385
|
-
}).catch((err) => {
|
|
6386
|
-
if (!cancelled) setState({
|
|
6387
|
-
report: null,
|
|
6388
|
-
loading: false,
|
|
6389
|
-
error: err instanceof Error ? err.message : String(err)
|
|
6390
|
-
});
|
|
6391
|
-
});
|
|
6392
|
-
return () => {
|
|
6393
|
-
cancelled = true;
|
|
6394
|
-
};
|
|
6395
|
-
}, []);
|
|
6396
|
-
return state;
|
|
6397
|
-
}
|
|
6398
|
-
// src/components/layout/use-capability-info.ts
|
|
6399
|
-
function deriveStatus(report, loading) {
|
|
6400
|
-
if (loading) return "checking";
|
|
6401
|
-
if (report == null) return "unsupported";
|
|
6402
|
-
if (report.opfs && report.composerSupported && report.issues.length === 0) return "ready";
|
|
6403
|
-
if (report.opfs && report.composerSupported) return "degraded";
|
|
6404
|
-
return "unsupported";
|
|
6405
|
-
}
|
|
6406
|
-
/** 能力探测的 UI 适配层:把 core 的报告转成状态与文案 */
|
|
6407
|
-
function useCapabilityInfo() {
|
|
6408
|
-
const { report, loading, error } = useCapability();
|
|
6409
|
-
return {
|
|
6410
|
-
report,
|
|
6411
|
-
loading,
|
|
6412
|
-
error,
|
|
6413
|
-
status: deriveStatus(report, loading),
|
|
6414
|
-
summary: (0, _cut_workbench_core_engine.summarizeCapabilities)(report)
|
|
6415
|
-
};
|
|
6416
|
-
}
|
|
6417
|
-
// src/features/lab/components/LabCard.tsx
|
|
6418
|
-
/** Lab 卡片外壳:标题 + 用到的 API + (不可用时的原因) */
|
|
6419
|
-
function LabCard({ title, description, apis, children, unavailable, className }) {
|
|
6420
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
|
|
6421
|
-
className: cn("flex flex-col", className),
|
|
6422
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CardHeader, {
|
|
6423
|
-
className: "gap-1",
|
|
6424
|
-
children: [
|
|
6425
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6426
|
-
className: "flex items-center gap-2",
|
|
6427
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6428
|
-
className: "flex size-6 items-center justify-center rounded bg-primary/10 text-primary",
|
|
6429
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.FlaskConical, { className: "size-3.5" })
|
|
6430
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardTitle, {
|
|
6431
|
-
className: "text-sm",
|
|
6432
|
-
children: title
|
|
6433
|
-
})]
|
|
6434
|
-
}),
|
|
6435
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardDescription, {
|
|
6436
|
-
className: "text-xs",
|
|
6437
|
-
children: description
|
|
6438
|
-
}),
|
|
6439
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6440
|
-
className: "flex flex-wrap gap-1 pt-1",
|
|
6441
|
-
children: apis.map((api) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
|
|
6442
|
-
variant: "outline",
|
|
6443
|
-
className: "font-mono text-[10px] font-normal text-muted-foreground",
|
|
6444
|
-
children: api
|
|
6445
|
-
}, api))
|
|
6446
|
-
})
|
|
6447
|
-
]
|
|
6448
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardContent, {
|
|
6449
|
-
className: "flex flex-1 flex-col gap-2.5",
|
|
6450
|
-
children: unavailable != null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6451
|
-
className: "rounded-md border border-dashed border-warning/40 bg-warning/5 p-3 text-xs text-warning",
|
|
6452
|
-
children: unavailable
|
|
6453
|
-
}) : children
|
|
6454
|
-
})]
|
|
6455
|
-
});
|
|
6456
|
-
}
|
|
6457
|
-
// src/features/lab/hooks/use-lab-action.ts
|
|
6458
|
-
/** 生成可下载链接(Lab 通用) */
|
|
6459
|
-
function useDownload() {
|
|
6460
|
-
const urlsRef = (0, react.useRef)([]);
|
|
6461
|
-
return (0, react.useCallback)((blob, filename) => {
|
|
6462
|
-
const url = URL.createObjectURL(blob);
|
|
6463
|
-
urlsRef.current.push(url);
|
|
6464
|
-
const link = document.createElement("a");
|
|
6465
|
-
link.href = url;
|
|
6466
|
-
link.download = filename;
|
|
6467
|
-
document.body.appendChild(link);
|
|
6468
|
-
link.click();
|
|
6469
|
-
link.remove();
|
|
6470
|
-
window.setTimeout(() => URL.revokeObjectURL(url), 15e3);
|
|
6471
|
-
}, []);
|
|
6472
|
-
}
|
|
6473
|
-
/** 带 busy 状态的异步执行器:统一错误 toast,禁止静默吞异常 */
|
|
6474
|
-
function useLabAction() {
|
|
6475
|
-
const [busy, setBusy] = (0, react.useState)(null);
|
|
6476
|
-
return {
|
|
6477
|
-
busy,
|
|
6478
|
-
run: (0, react.useCallback)(async (label, task) => {
|
|
6479
|
-
setBusy(label);
|
|
6480
|
-
try {
|
|
6481
|
-
return await task();
|
|
6482
|
-
} catch (err) {
|
|
6483
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
6484
|
-
console.error(err);
|
|
6485
|
-
sonner.toast.error("操作失败", { description: message });
|
|
6486
|
-
return null;
|
|
6487
|
-
} finally {
|
|
6488
|
-
setBusy(null);
|
|
6489
|
-
}
|
|
6490
|
-
}, [])
|
|
6491
|
-
};
|
|
6492
|
-
}
|
|
6493
|
-
// src/features/lab/components/shared.tsx
|
|
6494
|
-
/**
|
|
6495
|
-
* Lab 共享的可视组件。
|
|
6496
|
-
*
|
|
6497
|
-
* 只导出组件(React Fast Refresh 要求单文件只导出组件);
|
|
6498
|
-
* 工具函数与 hook 放在 `../hooks/use-lab-action`。
|
|
6499
|
-
*/
|
|
6500
|
-
/** 文件选择按钮 */
|
|
6501
|
-
function FilePickButton({ label, accept, multiple, onFiles, disabled }) {
|
|
6502
|
-
const inputRef = (0, react.useRef)(null);
|
|
6503
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
6504
|
-
ref: inputRef,
|
|
6505
|
-
type: "file",
|
|
6506
|
-
accept,
|
|
6507
|
-
multiple,
|
|
6508
|
-
className: "hidden",
|
|
6509
|
-
onChange: (e) => {
|
|
6510
|
-
const files = Array.from(e.target.files ?? []);
|
|
6511
|
-
if (files.length > 0) onFiles(files);
|
|
6512
|
-
e.target.value = "";
|
|
6513
|
-
}
|
|
6514
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6515
|
-
type: "button",
|
|
6516
|
-
disabled,
|
|
6517
|
-
onClick: () => inputRef.current?.click(),
|
|
6518
|
-
className: "flex w-full items-center justify-center gap-1.5 rounded-md border border-dashed px-3 py-3.5 text-xs text-muted-foreground transition-colors hover:border-primary/50 hover:text-foreground disabled:pointer-events-none disabled:opacity-50",
|
|
6519
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6520
|
-
className: "max-w-full truncate",
|
|
6521
|
-
children: label
|
|
6522
|
-
})
|
|
6523
|
-
})] });
|
|
6524
|
-
}
|
|
6525
|
-
/** 结果预览:视频 + 下载 */
|
|
6526
|
-
function ResultPreview({ blob, filename }) {
|
|
6527
|
-
const download = useDownload();
|
|
6528
|
-
const [url] = (0, react.useState)(() => URL.createObjectURL(blob));
|
|
6529
|
-
(0, react.useEffect)(() => () => URL.revokeObjectURL(url), [url]);
|
|
6530
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6531
|
-
className: "space-y-1.5",
|
|
6532
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("video", {
|
|
6533
|
-
src: url,
|
|
6534
|
-
controls: true,
|
|
6535
|
-
className: "w-full rounded-md border bg-stage-bg",
|
|
6536
|
-
style: { maxHeight: 200 }
|
|
6537
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6538
|
-
className: "flex items-center justify-between gap-2",
|
|
6539
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
6540
|
-
className: "min-w-0 flex-1 truncate text-[10px] text-muted-foreground",
|
|
6541
|
-
children: [(blob.size / 1024 / 1024).toFixed(2), " MB"]
|
|
6542
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6543
|
-
type: "button",
|
|
6544
|
-
onClick: () => download(blob, filename),
|
|
6545
|
-
className: "rounded-md bg-primary px-2 py-1 text-[11px] text-primary-foreground hover:bg-primary/90",
|
|
6546
|
-
children: "下载"
|
|
6547
|
-
})]
|
|
6548
|
-
})]
|
|
6549
|
-
});
|
|
6550
|
-
}
|
|
6551
|
-
/** 忙碌提示 */
|
|
6552
|
-
function Busy({ label }) {
|
|
6553
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6554
|
-
className: "flex items-center gap-2 rounded-md bg-muted px-3 py-2 text-xs text-muted-foreground",
|
|
6555
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Loader2, { className: "size-3 animate-spin" }), label]
|
|
6556
|
-
});
|
|
6557
|
-
}
|
|
6558
|
-
/** 简易滑杆 */
|
|
6559
|
-
function SliderRow({ label, value, min, max, step, format, onChange }) {
|
|
6560
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6561
|
-
className: "space-y-1",
|
|
6562
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6563
|
-
className: "flex justify-between text-[10px] text-muted-foreground",
|
|
6564
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6565
|
-
className: "font-mono tabular-nums",
|
|
6566
|
-
children: format ? format(value) : value.toFixed(2)
|
|
6567
|
-
})]
|
|
6568
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
6569
|
-
type: "range",
|
|
6570
|
-
min,
|
|
6571
|
-
max,
|
|
6572
|
-
step,
|
|
6573
|
-
value,
|
|
6574
|
-
onChange: (e) => onChange(Number(e.target.value)),
|
|
6575
|
-
className: "h-1 w-full cursor-pointer appearance-none rounded-full bg-muted accent-primary"
|
|
6576
|
-
})]
|
|
6577
|
-
});
|
|
6578
|
-
}
|
|
6579
|
-
// src/features/lab/components/ConcatenateLab.tsx
|
|
6580
|
-
/**
|
|
6581
|
-
* Lab 1:无损拼接。
|
|
6582
|
-
*
|
|
6583
|
-
* 用 `fastConcatMP4` 直接拼接多个 MP4 的样本,不重编码。
|
|
6584
|
-
* 前提是编码 / 分辨率 / 采样率完全一致;不一致时本 Lab 会先预检并给出具体差异。
|
|
6585
|
-
*/
|
|
6586
|
-
function ConcatenateLab() {
|
|
6587
|
-
const [files, setFiles] = (0, react.useState)([]);
|
|
6588
|
-
const [infos, setInfos] = (0, react.useState)([]);
|
|
6589
|
-
const [result, setResult] = (0, react.useState)(null);
|
|
6590
|
-
const { busy, run } = useLabAction();
|
|
6591
|
-
const handleFiles = async (picked) => {
|
|
6592
|
-
setFiles(picked);
|
|
6593
|
-
setResult(null);
|
|
6594
|
-
setInfos([]);
|
|
6595
|
-
const detected = await run("正在读取文件信息…", async () => {
|
|
6596
|
-
const list = [];
|
|
6597
|
-
for (const file of picked) list.push(await (0, _cut_workbench_core_lab.readMp4Info)(file));
|
|
6598
|
-
return list;
|
|
6599
|
-
});
|
|
6600
|
-
if (detected != null) setInfos(detected);
|
|
6601
|
-
};
|
|
6602
|
-
const handleConcat = async () => {
|
|
6603
|
-
const output = await run("正在无损拼接…", () => (0, _cut_workbench_core_lab.concatMp4)(files));
|
|
6604
|
-
if (output != null) {
|
|
6605
|
-
setResult(output.blob);
|
|
6606
|
-
setInfos(output.infos);
|
|
6607
|
-
}
|
|
6608
|
-
};
|
|
6609
|
-
const first = infos[0];
|
|
6610
|
-
const mismatch = first != null && infos.some((info) => info.width !== first.width || info.height !== first.height || info.codec !== first.codec || info.audioSampleRate !== first.audioSampleRate);
|
|
6611
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LabCard, {
|
|
6612
|
-
title: "无损拼接",
|
|
6613
|
-
description: "直接把多个 MP4 的样本拼在一起,不重编码,速度极快。要求规格完全一致。",
|
|
6614
|
-
apis: ["fastConcatMP4", "MP4Clip.getFileHeaderBinData"],
|
|
6615
|
-
children: [
|
|
6616
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FilePickButton, {
|
|
6617
|
-
label: files.length > 0 ? `已选 ${files.length} 个文件(重新选择)` : "选择 2 个以上 MP4",
|
|
6618
|
-
accept: "video/mp4",
|
|
6619
|
-
multiple: true,
|
|
6620
|
-
onFiles: (picked) => void handleFiles(picked),
|
|
6621
|
-
disabled: busy != null
|
|
6622
|
-
}),
|
|
6623
|
-
files.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
6624
|
-
className: "space-y-1 text-[10px] text-muted-foreground",
|
|
6625
|
-
children: files.map((file, index) => {
|
|
6626
|
-
const info = infos[index];
|
|
6627
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
6628
|
-
className: "flex justify-between gap-2",
|
|
6629
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6630
|
-
className: "truncate",
|
|
6631
|
-
children: file.name
|
|
6632
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6633
|
-
className: "shrink-0 font-mono",
|
|
6634
|
-
children: info != null ? `${formatResolution(info.width, info.height)} · ${(0, _cut_workbench_core.formatDuration)(info.durationUs)} · ${info.codec}` : "检测中…"
|
|
6635
|
-
})]
|
|
6636
|
-
}, `${file.name}-${index}`);
|
|
6637
|
-
})
|
|
6638
|
-
}),
|
|
6639
|
-
mismatch && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6640
|
-
className: "flex items-start gap-1.5 rounded-md bg-warning/10 p-2 text-[10px] text-warning",
|
|
6641
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.AlertTriangle, { className: "mt-0.5 size-3 shrink-0" }), "检测到规格不一致,拼接会失败并给出具体原因(fastConcatMP4 的硬性要求)。"]
|
|
6642
|
-
}),
|
|
6643
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6644
|
-
type: "button",
|
|
6645
|
-
disabled: files.length < 2 || busy != null,
|
|
6646
|
-
onClick: () => void handleConcat(),
|
|
6647
|
-
className: "rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground disabled:opacity-50",
|
|
6648
|
-
children: "开始拼接"
|
|
6649
|
-
}),
|
|
6650
|
-
busy != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Busy, { label: busy }),
|
|
6651
|
-
result != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResultPreview, {
|
|
6652
|
-
blob: result,
|
|
6653
|
-
filename: "concat.mp4"
|
|
6654
|
-
})
|
|
6655
|
-
]
|
|
6656
|
-
});
|
|
6657
|
-
}
|
|
6658
|
-
// src/features/lab/components/RemuxLab.tsx
|
|
6659
|
-
/**
|
|
6660
|
-
* Lab 2:配音。
|
|
6661
|
-
*
|
|
6662
|
-
* `mixinMP4AndAudio` 只重编码音频轨,视频轨原样复制 —— 因此速度快、画质无损。
|
|
6663
|
-
*/
|
|
6664
|
-
function RemuxLab() {
|
|
6665
|
-
const [video, setVideo] = (0, react.useState)(null);
|
|
6666
|
-
const [audio, setAudio] = (0, react.useState)(null);
|
|
6667
|
-
const [volume, setVolume] = (0, react.useState)(1);
|
|
6668
|
-
const [loop, setLoop] = (0, react.useState)(true);
|
|
6669
|
-
const [result, setResult] = (0, react.useState)(null);
|
|
6670
|
-
const { busy, run } = useLabAction();
|
|
6671
|
-
const handleMix = async () => {
|
|
6672
|
-
if (video == null || audio == null) return;
|
|
6673
|
-
const output = await run("正在混合音频…", () => (0, _cut_workbench_core_lab.mixAudioIntoMp4)(video, audio, volume, loop));
|
|
6674
|
-
if (output != null) setResult(output);
|
|
6675
|
-
};
|
|
6676
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LabCard, {
|
|
6677
|
-
title: "视频配音",
|
|
6678
|
-
description: "把音轨混入 MP4:只重编码音频,视频轨不动,画质零损失。",
|
|
6679
|
-
apis: ["mixinMP4AndAudio"],
|
|
6680
|
-
children: [
|
|
6681
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FilePickButton, {
|
|
6682
|
-
label: video == null ? "选择视频(MP4)" : video.name,
|
|
6683
|
-
accept: "video/mp4",
|
|
6684
|
-
onFiles: (files) => {
|
|
6685
|
-
setVideo(files[0] ?? null);
|
|
6686
|
-
setResult(null);
|
|
6687
|
-
},
|
|
6688
|
-
disabled: busy != null
|
|
6689
|
-
}),
|
|
6690
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FilePickButton, {
|
|
6691
|
-
label: audio == null ? "选择音频(MP3/WAV/M4A)" : audio.name,
|
|
6692
|
-
accept: "audio/*",
|
|
6693
|
-
onFiles: (files) => {
|
|
6694
|
-
setAudio(files[0] ?? null);
|
|
6695
|
-
setResult(null);
|
|
6696
|
-
},
|
|
6697
|
-
disabled: busy != null
|
|
6698
|
-
}),
|
|
6699
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderRow, {
|
|
6700
|
-
label: "音量",
|
|
6701
|
-
value: volume,
|
|
6702
|
-
min: 0,
|
|
6703
|
-
max: 2,
|
|
6704
|
-
step: .05,
|
|
6705
|
-
format: (v) => `${Math.round(v * 100)}%`,
|
|
6706
|
-
onChange: setVolume
|
|
6707
|
-
}),
|
|
6708
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
6709
|
-
className: "flex items-center gap-2 text-[11px] text-muted-foreground",
|
|
6710
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
6711
|
-
type: "checkbox",
|
|
6712
|
-
checked: loop,
|
|
6713
|
-
onChange: (e) => setLoop(e.target.checked),
|
|
6714
|
-
className: "accent-primary"
|
|
6715
|
-
}), "音频短于视频时循环填充"]
|
|
6716
|
-
}),
|
|
6717
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6718
|
-
type: "button",
|
|
6719
|
-
disabled: video == null || audio == null || busy != null,
|
|
6720
|
-
onClick: () => void handleMix(),
|
|
6721
|
-
className: "rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground disabled:opacity-50",
|
|
6722
|
-
children: "开始混合"
|
|
6723
|
-
}),
|
|
6724
|
-
busy != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Busy, { label: busy }),
|
|
6725
|
-
result != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResultPreview, {
|
|
6726
|
-
blob: result,
|
|
6727
|
-
filename: "mixin.mp4"
|
|
6728
|
-
})
|
|
6729
|
-
]
|
|
6730
|
-
});
|
|
6731
|
-
}
|
|
6732
|
-
// src/features/lab/components/ChromakeyLab.tsx
|
|
6733
|
-
/**
|
|
6734
|
-
* Lab 3:绿幕抠像。
|
|
6735
|
-
*
|
|
6736
|
-
* `createChromakey` 返回一个函数,把 VideoFrame 转成去背后的 ImageBitmap。
|
|
6737
|
-
* 配合 `MP4Clip.tickInterceptor` 即可对整段视频生效(导出时同样有效)。
|
|
6738
|
-
*
|
|
6739
|
-
* ⚠️ 依赖 WebGL2;不支持时本卡片隐藏操作、仅显示原因。
|
|
6740
|
-
*/
|
|
6741
|
-
/**
|
|
6742
|
-
* 抠像结果的兜底底色:读取主题令牌,而不是写死 `#101018`。
|
|
6743
|
-
*
|
|
6744
|
-
* 抠像后的 VideoFrame 带透明通道,需要一层底色才能看清;用 `--cw-stage-bg`
|
|
6745
|
-
* 让 Lab 预览区与工作台舞台保持一致,宿主换主题时一起变。
|
|
6746
|
-
* 取不到时退回深色(纯逻辑环境 / 未包 `.cw-root`)。
|
|
6747
|
-
*/
|
|
6748
|
-
function readStageBackground() {
|
|
6749
|
-
if (typeof document === "undefined") return "#101018";
|
|
6750
|
-
const root = document.querySelector(".cw-root");
|
|
6751
|
-
if (root == null) return "#101018";
|
|
6752
|
-
const value = getComputedStyle(root).getPropertyValue("--cw-stage-bg").trim();
|
|
6753
|
-
return value.length > 0 ? value : "#101018";
|
|
6754
|
-
}
|
|
6755
|
-
function ChromakeyLab() {
|
|
6756
|
-
const [webgl2] = (0, react.useState)(_cut_workbench_core_lab.isWebGL2Available);
|
|
6757
|
-
const [file, setFile] = (0, react.useState)(null);
|
|
6758
|
-
const [similarity, setSimilarity] = (0, react.useState)(.4);
|
|
6759
|
-
const [smoothness, setSmoothness] = (0, react.useState)(.1);
|
|
6760
|
-
const [spill, setSpill] = (0, react.useState)(.1);
|
|
6761
|
-
const [timeUs, setTimeUs] = (0, react.useState)(0);
|
|
6762
|
-
const [durationUs, setDurationUs] = (0, react.useState)(0);
|
|
6763
|
-
const canvasRef = (0, react.useRef)(null);
|
|
6764
|
-
const clipRef = (0, react.useRef)(null);
|
|
6765
|
-
const { busy, run } = useLabAction();
|
|
6766
|
-
(0, react.useEffect)(() => {
|
|
6767
|
-
return () => {
|
|
6768
|
-
clipRef.current?.destroy();
|
|
6769
|
-
clipRef.current = null;
|
|
6770
|
-
};
|
|
6771
|
-
}, []);
|
|
6772
|
-
const render = (0, react.useCallback)(async () => {
|
|
6773
|
-
const clip = clipRef.current;
|
|
6774
|
-
const canvas = canvasRef.current;
|
|
6775
|
-
if (clip == null || canvas == null) return;
|
|
6776
|
-
await (0, _cut_workbench_core_lab.renderChromakeyFrame)(clip, timeUs, {
|
|
6777
|
-
similarity,
|
|
6778
|
-
smoothness,
|
|
6779
|
-
spill,
|
|
6780
|
-
keyColor: [
|
|
6781
|
-
0,
|
|
6782
|
-
255,
|
|
6783
|
-
0
|
|
6784
|
-
]
|
|
6785
|
-
}, canvas, readStageBackground());
|
|
6786
|
-
}, [
|
|
6787
|
-
similarity,
|
|
6788
|
-
smoothness,
|
|
6789
|
-
spill,
|
|
6790
|
-
timeUs
|
|
6791
|
-
]);
|
|
6792
|
-
const handleFile = async (files) => {
|
|
6793
|
-
const picked = files[0];
|
|
6794
|
-
if (picked == null) return;
|
|
6795
|
-
const result = await run("正在解析视频…", async () => {
|
|
6796
|
-
clipRef.current?.destroy();
|
|
6797
|
-
return await (0, _cut_workbench_core_lab.createMp4Clip)(picked);
|
|
6798
|
-
});
|
|
6799
|
-
if (result == null) return;
|
|
6800
|
-
clipRef.current = result.clip;
|
|
6801
|
-
setFile(picked);
|
|
6802
|
-
setDurationUs(result.meta.duration);
|
|
6803
|
-
setTimeUs(Math.min(result.meta.duration / 2, 1e6));
|
|
6804
|
-
sonner.toast.success("已载入视频,拖动参数查看抠像效果");
|
|
6805
|
-
};
|
|
6806
|
-
(0, react.useEffect)(() => {
|
|
6807
|
-
if (clipRef.current == null || busy != null) return;
|
|
6808
|
-
render();
|
|
6809
|
-
}, [busy, render]);
|
|
6810
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LabCard, {
|
|
6811
|
-
title: "绿幕抠像",
|
|
6812
|
-
description: "用 WebGL2 实时扣除背景色,通过 tickInterceptor 作用到整段视频。",
|
|
6813
|
-
apis: ["createChromakey", "MP4Clip.tickInterceptor"],
|
|
6814
|
-
unavailable: webgl2 ? null : "当前设备不支持 WebGL2,createChromakey 无法运行。",
|
|
6815
|
-
children: [
|
|
6816
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FilePickButton, {
|
|
6817
|
-
label: file?.name ?? "选择绿幕视频(MP4)",
|
|
6818
|
-
accept: "video/mp4",
|
|
6819
|
-
onFiles: (files) => void handleFile(files),
|
|
6820
|
-
disabled: busy != null
|
|
6821
|
-
}),
|
|
6822
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6823
|
-
className: "relative overflow-hidden rounded-md border bg-stage-bg",
|
|
6824
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
|
|
6825
|
-
ref: canvasRef,
|
|
6826
|
-
className: "block w-full",
|
|
6827
|
-
style: { maxHeight: 180 }
|
|
6828
|
-
}), file == null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6829
|
-
className: "pointer-events-none absolute inset-0 flex h-24 items-center justify-center text-[10px] text-muted-foreground",
|
|
6830
|
-
children: "选择视频后显示抠像结果"
|
|
6831
|
-
})]
|
|
6832
|
-
}),
|
|
6833
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderRow, {
|
|
6834
|
-
label: "相似度 similarity",
|
|
6835
|
-
value: similarity,
|
|
6836
|
-
min: 0,
|
|
6837
|
-
max: 1,
|
|
6838
|
-
step: .01,
|
|
6839
|
-
format: (v) => v.toFixed(2),
|
|
6840
|
-
onChange: setSimilarity
|
|
6841
|
-
}),
|
|
6842
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderRow, {
|
|
6843
|
-
label: "平滑度 smoothness",
|
|
6844
|
-
value: smoothness,
|
|
6845
|
-
min: 0,
|
|
6846
|
-
max: 1,
|
|
6847
|
-
step: .01,
|
|
6848
|
-
format: (v) => v.toFixed(2),
|
|
6849
|
-
onChange: setSmoothness
|
|
6850
|
-
}),
|
|
6851
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderRow, {
|
|
6852
|
-
label: "饱和度 spill",
|
|
6853
|
-
value: spill,
|
|
6854
|
-
min: 0,
|
|
6855
|
-
max: 1,
|
|
6856
|
-
step: .01,
|
|
6857
|
-
format: (v) => v.toFixed(2),
|
|
6858
|
-
onChange: setSpill
|
|
6859
|
-
}),
|
|
6860
|
-
durationUs > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderRow, {
|
|
6861
|
-
label: "取帧时间",
|
|
6862
|
-
value: timeUs,
|
|
6863
|
-
min: 0,
|
|
6864
|
-
max: Math.max(1, durationUs - 1),
|
|
6865
|
-
step: 1e5,
|
|
6866
|
-
format: (v) => `${(v / 1e6).toFixed(2)}s`,
|
|
6867
|
-
onChange: setTimeUs
|
|
6868
|
-
}),
|
|
6869
|
-
busy != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Busy, { label: busy }),
|
|
6870
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
6871
|
-
className: "text-[10px] leading-relaxed text-muted-foreground",
|
|
6872
|
-
children: "keyColor 固定为纯绿 (0,255,0)。实际项目中可读取首帧像素自动取色。"
|
|
6873
|
-
})
|
|
6874
|
-
]
|
|
6875
|
-
});
|
|
6876
|
-
}
|
|
6877
|
-
// src/features/lab/components/SubtitleLab.tsx
|
|
6878
|
-
var SAMPLE_SRT = `1
|
|
6879
|
-
00:00:00,000 --> 00:00:02,000
|
|
6880
|
-
这是第一句字幕
|
|
6881
|
-
|
|
6882
|
-
2
|
|
6883
|
-
00:00:02,000 --> 00:00:04,000
|
|
6884
|
-
第二句会带描边
|
|
6885
|
-
|
|
6886
|
-
3
|
|
6887
|
-
00:00:04,000 --> 00:00:06,000
|
|
6888
|
-
EmbedSubtitlesClip 烧录进画面
|
|
6889
|
-
`;
|
|
6890
|
-
/**
|
|
6891
|
-
* Lab 4:字幕烧录。
|
|
6892
|
-
*
|
|
6893
|
-
* `EmbedSubtitlesClip` 本身就是一个 IClip:tick 返回带字幕的 VideoFrame。
|
|
6894
|
-
* 因此既能放进 Combinator 导出,也能直接逐帧预览。
|
|
6895
|
-
*/
|
|
6896
|
-
function SubtitleLab() {
|
|
6897
|
-
const [srt, setSrt] = (0, react.useState)(SAMPLE_SRT);
|
|
6898
|
-
const [timeUs, setTimeUs] = (0, react.useState)(0);
|
|
6899
|
-
const [playing, setPlaying] = (0, react.useState)(false);
|
|
6900
|
-
const [result, setResult] = (0, react.useState)(null);
|
|
6901
|
-
const { busy, run } = useLabAction();
|
|
6902
|
-
const canvasRef = (0, react.useRef)(null);
|
|
6903
|
-
const clipRef = (0, react.useRef)(null);
|
|
6904
|
-
const rafRef = (0, react.useRef)(null);
|
|
6905
|
-
const cues = (0, _cut_workbench_core.parseSrt)(srt);
|
|
6906
|
-
const durationUs = cues.length > 0 ? cues[cues.length - 1].endMs * 1e3 : 0;
|
|
6907
|
-
(0, react.useEffect)(() => {
|
|
6908
|
-
clipRef.current?.destroy();
|
|
6909
|
-
if (cues.length === 0) {
|
|
6910
|
-
clipRef.current = null;
|
|
6911
|
-
return;
|
|
6912
|
-
}
|
|
6913
|
-
try {
|
|
6914
|
-
clipRef.current = (0, _cut_workbench_core_lab.createSubtitlePreviewClip)(srt, 640, 360);
|
|
6915
|
-
} catch (err) {
|
|
6916
|
-
console.warn("[SubtitleLab] create failed", err);
|
|
6917
|
-
clipRef.current = null;
|
|
6918
|
-
}
|
|
6919
|
-
return () => {
|
|
6920
|
-
clipRef.current?.destroy();
|
|
6921
|
-
clipRef.current = null;
|
|
6922
|
-
};
|
|
6923
|
-
}, [cues.length, srt]);
|
|
6924
|
-
const draw = (0, react.useCallback)(async () => {
|
|
6925
|
-
const clip = clipRef.current;
|
|
6926
|
-
const canvas = canvasRef.current;
|
|
6927
|
-
if (clip == null || canvas == null) return;
|
|
6928
|
-
await (0, _cut_workbench_core_lab.renderSubtitleFrame)(clip, timeUs, canvas);
|
|
6929
|
-
}, [timeUs]);
|
|
6930
|
-
(0, react.useEffect)(() => {
|
|
6931
|
-
draw();
|
|
6932
|
-
}, [draw]);
|
|
6933
|
-
(0, react.useEffect)(() => {
|
|
6934
|
-
if (!playing) return;
|
|
6935
|
-
let last = performance.now();
|
|
6936
|
-
let current = timeUs;
|
|
6937
|
-
const tick = () => {
|
|
6938
|
-
const now = performance.now();
|
|
6939
|
-
current += (now - last) * 1e3;
|
|
6940
|
-
last = now;
|
|
6941
|
-
if (durationUs > 0 && current >= durationUs) {
|
|
6942
|
-
setTimeUs(durationUs);
|
|
6943
|
-
setPlaying(false);
|
|
6944
|
-
return;
|
|
6945
|
-
}
|
|
6946
|
-
setTimeUs(current);
|
|
6947
|
-
rafRef.current = requestAnimationFrame(tick);
|
|
6948
|
-
};
|
|
6949
|
-
rafRef.current = requestAnimationFrame(tick);
|
|
6950
|
-
return () => {
|
|
6951
|
-
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
|
|
6952
|
-
};
|
|
6953
|
-
}, [playing, durationUs]);
|
|
6954
|
-
const handleBurn = async () => {
|
|
6955
|
-
const width = 1280;
|
|
6956
|
-
const height = 720;
|
|
6957
|
-
const output = await run("正在烧录字幕…", () => (0, _cut_workbench_core_lab.burnSubtitles)(srt, width, height, durationUs));
|
|
6958
|
-
if (output != null) setResult(output);
|
|
6959
|
-
};
|
|
6960
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LabCard, {
|
|
6961
|
-
title: "字幕烧录",
|
|
6962
|
-
description: "SRT 解析后逐帧绘制到画面,最终编码进视频流(硬字幕)。",
|
|
6963
|
-
apis: ["EmbedSubtitlesClip", "Combinator"],
|
|
6964
|
-
children: [
|
|
6965
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
6966
|
-
value: srt,
|
|
6967
|
-
onChange: (e) => setSrt(e.target.value),
|
|
6968
|
-
rows: 5,
|
|
6969
|
-
className: "w-full rounded-md border bg-transparent p-2 font-mono text-[10px] outline-none focus-visible:border-ring",
|
|
6970
|
-
placeholder: "粘贴 SRT 内容"
|
|
6971
|
-
}),
|
|
6972
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6973
|
-
className: "overflow-hidden rounded-md border bg-stage-bg",
|
|
6974
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
|
|
6975
|
-
ref: canvasRef,
|
|
6976
|
-
width: 640,
|
|
6977
|
-
height: 360,
|
|
6978
|
-
className: "block w-full",
|
|
6979
|
-
style: { maxHeight: 180 }
|
|
6980
|
-
})
|
|
6981
|
-
}),
|
|
6982
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6983
|
-
className: "flex items-center gap-2",
|
|
6984
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6985
|
-
type: "button",
|
|
6986
|
-
onClick: () => setPlaying((p) => !p),
|
|
6987
|
-
disabled: cues.length === 0,
|
|
6988
|
-
className: "flex items-center gap-1 rounded-md bg-secondary px-2 py-1 text-[11px] disabled:opacity-50",
|
|
6989
|
-
children: [playing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Square, { className: "size-3" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Play, { className: "size-3" }), playing ? "停止" : "预览"]
|
|
6990
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
6991
|
-
className: "text-[10px] text-muted-foreground",
|
|
6992
|
-
children: [
|
|
6993
|
-
cues.length,
|
|
6994
|
-
" 条 · ",
|
|
6995
|
-
(timeUs / 1e6).toFixed(2),
|
|
6996
|
-
"s / ",
|
|
6997
|
-
(durationUs / 1e6).toFixed(2),
|
|
6998
|
-
"s"
|
|
6999
|
-
]
|
|
7000
|
-
})]
|
|
7001
|
-
}),
|
|
7002
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderRow, {
|
|
7003
|
-
label: "时间",
|
|
7004
|
-
value: timeUs,
|
|
7005
|
-
min: 0,
|
|
7006
|
-
max: Math.max(1, durationUs),
|
|
7007
|
-
step: 1e4,
|
|
7008
|
-
format: (v) => `${(v / 1e6).toFixed(2)}s`,
|
|
7009
|
-
onChange: (v) => {
|
|
7010
|
-
setPlaying(false);
|
|
7011
|
-
setTimeUs(v);
|
|
7012
|
-
}
|
|
7013
|
-
}),
|
|
7014
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7015
|
-
type: "button",
|
|
7016
|
-
disabled: cues.length === 0 || busy != null,
|
|
7017
|
-
onClick: () => void handleBurn(),
|
|
7018
|
-
className: "rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground disabled:opacity-50",
|
|
7019
|
-
children: "烧录并导出 MP4"
|
|
7020
|
-
}),
|
|
7021
|
-
busy != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Busy, { label: busy }),
|
|
7022
|
-
result != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResultPreview, {
|
|
7023
|
-
blob: result,
|
|
7024
|
-
filename: "subtitles.mp4"
|
|
7025
|
-
})
|
|
7026
|
-
]
|
|
7027
|
-
});
|
|
7028
|
-
}
|
|
7029
|
-
// src/features/lab/components/TrackSplitLab.tsx
|
|
7030
|
-
/**
|
|
7031
|
-
* Lab 5:轨道拆分。
|
|
7032
|
-
*
|
|
7033
|
-
* `splitTrack()` 从一个 MP4Clip 派生出「纯视频」与「纯音频」两个 clip,
|
|
7034
|
-
* 它们与源共享样本表,因此内存开销很小。
|
|
7035
|
-
*/
|
|
7036
|
-
function TrackSplitLab() {
|
|
7037
|
-
const [file, setFile] = (0, react.useState)(null);
|
|
7038
|
-
const [result, setResult] = (0, react.useState)(null);
|
|
7039
|
-
const { busy, run } = useLabAction();
|
|
7040
|
-
const cleanupRef = (0, react.useRef)(null);
|
|
7041
|
-
(0, react.useEffect)(() => {
|
|
7042
|
-
return () => cleanupRef.current?.();
|
|
7043
|
-
}, []);
|
|
7044
|
-
const handleSplit = async () => {
|
|
7045
|
-
if (file == null) return;
|
|
7046
|
-
cleanupRef.current?.();
|
|
7047
|
-
const output = await run("正在拆分轨道…", () => (0, _cut_workbench_core_lab.splitMp4Tracks)(file));
|
|
7048
|
-
setResult(output);
|
|
7049
|
-
cleanupRef.current = () => {
|
|
7050
|
-
output?.videoClip?.destroy();
|
|
7051
|
-
output?.audioClip?.destroy();
|
|
7052
|
-
};
|
|
7053
|
-
};
|
|
7054
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LabCard, {
|
|
7055
|
-
title: "轨道拆分",
|
|
7056
|
-
description: "把一个 MP4 拆成纯视频轨与纯音频轨两个独立 IClip,用于分别编辑。",
|
|
7057
|
-
apis: ["MP4Clip.splitTrack"],
|
|
7058
|
-
children: [
|
|
7059
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FilePickButton, {
|
|
7060
|
-
label: file?.name ?? "选择 MP4",
|
|
7061
|
-
accept: "video/mp4",
|
|
7062
|
-
onFiles: (files) => {
|
|
7063
|
-
setFile(files[0] ?? null);
|
|
7064
|
-
setResult(null);
|
|
7065
|
-
},
|
|
7066
|
-
disabled: busy != null
|
|
7067
|
-
}),
|
|
7068
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
7069
|
-
type: "button",
|
|
7070
|
-
disabled: file == null || busy != null,
|
|
7071
|
-
onClick: () => void handleSplit(),
|
|
7072
|
-
className: "flex items-center justify-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground disabled:opacity-50",
|
|
7073
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Split, { className: "size-3" }), "拆分轨道"]
|
|
7074
|
-
}),
|
|
7075
|
-
busy != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Busy, { label: busy }),
|
|
7076
|
-
result != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7077
|
-
className: "grid gap-1.5 text-[10px]",
|
|
7078
|
-
children: [
|
|
7079
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrackResult, {
|
|
7080
|
-
label: "视频轨",
|
|
7081
|
-
present: result.videoMeta != null,
|
|
7082
|
-
detail: result.videoMeta != null ? `${result.videoMeta.width}×${result.videoMeta.height} · ${(0, _cut_workbench_core.formatDuration)(result.videoMeta.duration)}` : "源文件无视频轨"
|
|
7083
|
-
}),
|
|
7084
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrackResult, {
|
|
7085
|
-
label: "音频轨",
|
|
7086
|
-
present: result.audioMeta != null,
|
|
7087
|
-
detail: result.audioMeta != null ? `48kHz 双声道 · ${(0, _cut_workbench_core.formatDuration)(result.audioMeta.duration)}` : "源文件无音频轨"
|
|
7088
|
-
}),
|
|
7089
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7090
|
-
className: "pt-1 leading-relaxed text-muted-foreground",
|
|
7091
|
-
children: "拆分出的 clip 与源共享样本表,可分别创建 Sprite 放到不同轨道;两者在时间轴上对齐即等于原片。"
|
|
7092
|
-
})
|
|
7093
|
-
]
|
|
7094
|
-
})
|
|
7095
|
-
]
|
|
7096
|
-
});
|
|
7097
|
-
}
|
|
7098
|
-
function TrackResult({ label, present, detail }) {
|
|
7099
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7100
|
-
className: "flex items-center justify-between gap-2 rounded-md border px-2 py-1.5",
|
|
7101
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7102
|
-
className: present ? "text-foreground" : "text-muted-foreground",
|
|
7103
|
-
children: label
|
|
7104
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7105
|
-
className: present ? "font-mono text-success" : "text-muted-foreground",
|
|
7106
|
-
children: present ? detail : detail
|
|
7107
|
-
})]
|
|
7108
|
-
});
|
|
7109
|
-
}
|
|
7110
|
-
// src/features/lab/components/ThumbnailLab.tsx
|
|
7111
|
-
/**
|
|
7112
|
-
* Lab 6:缩略图抽帧。
|
|
7113
|
-
*
|
|
7114
|
-
* `MP4Clip.thumbnails()` 默认每个关键帧生成一张;也可以传 step 按固定间隔取。
|
|
7115
|
-
* 这里展示全部关键帧与时间戳。
|
|
7116
|
-
*/
|
|
7117
|
-
function ThumbnailLab() {
|
|
7118
|
-
const [file, setFile] = (0, react.useState)(null);
|
|
7119
|
-
const [frames, setFrames] = (0, react.useState)([]);
|
|
7120
|
-
const [width, setWidth] = (0, react.useState)(120);
|
|
7121
|
-
const { busy, run } = useLabAction();
|
|
7122
|
-
(0, react.useEffect)(() => {
|
|
7123
|
-
return () => {
|
|
7124
|
-
for (const frame of frames) URL.revokeObjectURL(frame.url);
|
|
7125
|
-
};
|
|
7126
|
-
}, [frames]);
|
|
7127
|
-
const handleExtract = async () => {
|
|
7128
|
-
if (file == null) return;
|
|
7129
|
-
const output = await run("正在抽取关键帧…", () => (0, _cut_workbench_core_lab.extractKeyframes)(file, width));
|
|
7130
|
-
if (output != null) setFrames(output);
|
|
7131
|
-
};
|
|
7132
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LabCard, {
|
|
7133
|
-
title: "缩略图抽帧",
|
|
7134
|
-
description: "按关键帧抽取缩略图,可用于时间轴缩略图带或场景切分预览。",
|
|
7135
|
-
apis: ["MP4Clip.thumbnails"],
|
|
7136
|
-
children: [
|
|
7137
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FilePickButton, {
|
|
7138
|
-
label: file?.name ?? "选择 MP4",
|
|
7139
|
-
accept: "video/mp4",
|
|
7140
|
-
onFiles: (files) => {
|
|
7141
|
-
setFile(files[0] ?? null);
|
|
7142
|
-
setFrames([]);
|
|
7143
|
-
},
|
|
7144
|
-
disabled: busy != null
|
|
7145
|
-
}),
|
|
7146
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7147
|
-
className: "flex items-center gap-2",
|
|
7148
|
-
children: [
|
|
7149
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
7150
|
-
className: "text-[10px] text-muted-foreground",
|
|
7151
|
-
children: "宽度"
|
|
7152
|
-
}),
|
|
7153
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
7154
|
-
type: "number",
|
|
7155
|
-
min: 40,
|
|
7156
|
-
max: 480,
|
|
7157
|
-
step: 20,
|
|
7158
|
-
value: width,
|
|
7159
|
-
onChange: (e) => setWidth(Number(e.target.value)),
|
|
7160
|
-
className: "h-6 w-16 rounded border bg-transparent px-1.5 text-[11px]"
|
|
7161
|
-
}),
|
|
7162
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7163
|
-
type: "button",
|
|
7164
|
-
disabled: file == null || busy != null,
|
|
7165
|
-
onClick: () => void handleExtract(),
|
|
7166
|
-
className: "ml-auto rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground disabled:opacity-50",
|
|
7167
|
-
children: "抽取"
|
|
7168
|
-
})
|
|
7169
|
-
]
|
|
7170
|
-
}),
|
|
7171
|
-
busy != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Busy, { label: busy }),
|
|
7172
|
-
frames.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
7173
|
-
className: "text-[10px] text-muted-foreground",
|
|
7174
|
-
children: [
|
|
7175
|
-
"共 ",
|
|
7176
|
-
frames.length,
|
|
7177
|
-
" 张关键帧"
|
|
7178
|
-
]
|
|
7179
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7180
|
-
className: "thin-scrollbar grid max-h-52 grid-cols-3 gap-1.5 overflow-auto",
|
|
7181
|
-
children: frames.map((frame, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", {
|
|
7182
|
-
className: "space-y-0.5",
|
|
7183
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
7184
|
-
src: frame.url,
|
|
7185
|
-
alt: "",
|
|
7186
|
-
className: "w-full rounded border object-cover",
|
|
7187
|
-
loading: "lazy"
|
|
7188
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("figcaption", {
|
|
7189
|
-
className: "font-mono text-[9px] text-muted-foreground tabular-nums",
|
|
7190
|
-
children: (0, _cut_workbench_core.formatTimecode)(frame.ts)
|
|
7191
|
-
})]
|
|
7192
|
-
}, `${frame.ts}-${index}`))
|
|
7193
|
-
})] })
|
|
7194
|
-
]
|
|
7195
|
-
});
|
|
7196
|
-
}
|
|
7197
|
-
// src/features/lab/components/RecorderLab.tsx
|
|
7198
|
-
/**
|
|
7199
|
-
* Lab 7:屏幕 / 画布录制。
|
|
6350
|
+
* |---|---|
|
|
6351
|
+
* | Space | 播放 / 暂停 |
|
|
6352
|
+
* | S | 在播放头分割选中片段(未选中则分割命中的全部片段) |
|
|
6353
|
+
* | Delete / Backspace | 删除选中片段 |
|
|
6354
|
+
* | Ctrl+Z / Ctrl+Shift+Z | 撤销 / 重做 |
|
|
6355
|
+
* | Ctrl+D | 复制选中片段 |
|
|
6356
|
+
* | Ctrl+A | 全选 |
|
|
6357
|
+
* | ← / → | 逐帧(1/30 s) |
|
|
6358
|
+
* | Home / End | 跳首 / 跳尾 |
|
|
6359
|
+
* | + / - | 时间轴缩放 |
|
|
6360
|
+
* | Esc | 清空选择 |
|
|
7200
6361
|
*
|
|
7201
|
-
*
|
|
7202
|
-
*
|
|
7203
|
-
* - 程序化画布(Canvas.captureStream,无需授权);
|
|
7204
|
-
* - 摄像头(getUserMedia,需要用户授权)。
|
|
6362
|
+
* 设计:编辑类操作直接调 store;播放类操作通过 `cut-test:*` 自定义事件通知预览面板,
|
|
6363
|
+
* 避免 store → features 的反向依赖。
|
|
7205
6364
|
*/
|
|
7206
|
-
function
|
|
7207
|
-
const
|
|
7208
|
-
const
|
|
7209
|
-
const
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
const { run } = useLabAction();
|
|
7219
|
-
(0, react.useEffect)(() => {
|
|
7220
|
-
const video = videoRef.current;
|
|
7221
|
-
if (video == null) return;
|
|
7222
|
-
video.srcObject = previewStream;
|
|
7223
|
-
if (previewStream != null) video.play().catch(() => void 0);
|
|
7224
|
-
return () => {
|
|
7225
|
-
if (video.srcObject === previewStream) video.srcObject = null;
|
|
7226
|
-
};
|
|
7227
|
-
}, [previewStream]);
|
|
7228
|
-
const cleanup = (0, react.useCallback)(() => {
|
|
7229
|
-
demoRef.current?.stop();
|
|
7230
|
-
demoRef.current = null;
|
|
7231
|
-
cameraRef.current?.getTracks().forEach((t) => t.stop());
|
|
7232
|
-
cameraRef.current = null;
|
|
7233
|
-
recorderRef.current?.dispose();
|
|
7234
|
-
recorderRef.current = null;
|
|
7235
|
-
setPreviewStream(null);
|
|
7236
|
-
}, []);
|
|
7237
|
-
(0, react.useEffect)(() => cleanup, [cleanup]);
|
|
7238
|
-
const acquireStream = async () => {
|
|
7239
|
-
cleanup();
|
|
7240
|
-
if (mode === "demo") {
|
|
7241
|
-
const demo = (0, _cut_workbench_core_lab.createDemoCanvasStream)(1280, 720);
|
|
7242
|
-
demoRef.current = demo;
|
|
7243
|
-
setPreviewStream(demo.stream);
|
|
7244
|
-
return demo.stream;
|
|
6365
|
+
function useKeyboardShortcuts() {
|
|
6366
|
+
const editor = (0, _cut_workbench_react.useCutEditor)();
|
|
6367
|
+
const uiApi = require_notify.useUiStoreApi();
|
|
6368
|
+
const handler = (0, react.useCallback)((event) => {
|
|
6369
|
+
const target = event.target;
|
|
6370
|
+
const isEditable = target != null && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable || target.getAttribute("role") === "textbox" || target.closest("[role=menu]") != null);
|
|
6371
|
+
const mod = event.ctrlKey || event.metaKey;
|
|
6372
|
+
if (mod && event.key.toLowerCase() === "z") {
|
|
6373
|
+
event.preventDefault();
|
|
6374
|
+
if (event.shiftKey) editor.store.getState().redo();
|
|
6375
|
+
else editor.store.getState().undo();
|
|
6376
|
+
return;
|
|
7245
6377
|
}
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
});
|
|
7251
|
-
cameraRef.current = stream;
|
|
7252
|
-
setPreviewStream(stream);
|
|
7253
|
-
return stream;
|
|
7254
|
-
} catch (err) {
|
|
7255
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
7256
|
-
sonner.toast.error("无法访问摄像头", { description: message });
|
|
7257
|
-
return null;
|
|
6378
|
+
if (mod && event.key.toLowerCase() === "d") {
|
|
6379
|
+
event.preventDefault();
|
|
6380
|
+
editor.store.getState().duplicateSelection(5e5);
|
|
6381
|
+
return;
|
|
7258
6382
|
}
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
setResult(null);
|
|
7265
|
-
const recorder = createRecorderAdapter(stream, (total) => setBytes(total));
|
|
7266
|
-
recorderRef.current = recorder;
|
|
7267
|
-
recorder.start();
|
|
7268
|
-
setRecording(true);
|
|
7269
|
-
setPaused(false);
|
|
7270
|
-
setState(recorder.state());
|
|
7271
|
-
};
|
|
7272
|
-
const handlePauseToggle = () => {
|
|
7273
|
-
const recorder = recorderRef.current;
|
|
7274
|
-
if (recorder == null) return;
|
|
7275
|
-
if (paused) {
|
|
7276
|
-
recorder.resume();
|
|
7277
|
-
setPaused(false);
|
|
7278
|
-
} else {
|
|
7279
|
-
recorder.pause();
|
|
7280
|
-
setPaused(true);
|
|
6383
|
+
if (mod && event.key.toLowerCase() === "a" && !isEditable) {
|
|
6384
|
+
event.preventDefault();
|
|
6385
|
+
const state = editor.store.getState();
|
|
6386
|
+
state.selectClips(state.project.clips.map((c) => c.id));
|
|
6387
|
+
return;
|
|
7281
6388
|
}
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
const raw = await recorder.stop();
|
|
7289
|
-
return (0, _cut_workbench_core_lab.fixRecordedDuration)(raw);
|
|
7290
|
-
});
|
|
7291
|
-
setRecording(false);
|
|
7292
|
-
setPaused(false);
|
|
7293
|
-
setState(recorder.state());
|
|
7294
|
-
recorderRef.current = null;
|
|
7295
|
-
if (output != null) {
|
|
7296
|
-
setResult(output);
|
|
7297
|
-
sonner.toast.success(`录制完成 · ${(output.size / 1024 / 1024).toFixed(2)} MB`);
|
|
6389
|
+
if (mod && (event.key === "+" || event.key === "=" || event.key === "-" || event.key === "_")) {
|
|
6390
|
+
event.preventDefault();
|
|
6391
|
+
const ui = uiApi.getState();
|
|
6392
|
+
const factor = event.key === "-" || event.key === "_" ? 1 / 1.25 : 1.25;
|
|
6393
|
+
ui.setPxPerSecond((0, _cut_workbench_core.clamp)(ui.pxPerSecond * factor, 10, 500));
|
|
6394
|
+
return;
|
|
7298
6395
|
}
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
}
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
}
|
|
7358
|
-
|
|
7359
|
-
function createRecorderAdapter(stream, onChunk) {
|
|
7360
|
-
const recorder = new _cut_workbench_core_lab.AVRecorder(stream, { expectFPS: 30 });
|
|
7361
|
-
const chunks = [];
|
|
7362
|
-
let total = 0;
|
|
7363
|
-
return {
|
|
7364
|
-
start: () => {
|
|
7365
|
-
const readable = recorder.start(300);
|
|
7366
|
-
(async () => {
|
|
7367
|
-
const reader = readable.getReader();
|
|
7368
|
-
for (;;) try {
|
|
7369
|
-
const { done, value } = await reader.read();
|
|
7370
|
-
if (done) break;
|
|
7371
|
-
if (value != null) {
|
|
7372
|
-
chunks.push(new Uint8Array(value));
|
|
7373
|
-
total += value.byteLength;
|
|
7374
|
-
onChunk(total);
|
|
7375
|
-
}
|
|
7376
|
-
} catch {
|
|
7377
|
-
break;
|
|
7378
|
-
}
|
|
7379
|
-
})();
|
|
7380
|
-
},
|
|
7381
|
-
pause: () => recorder.pause(),
|
|
7382
|
-
resume: () => recorder.resume(),
|
|
7383
|
-
stop: async () => {
|
|
7384
|
-
await recorder.stop();
|
|
7385
|
-
return new Blob(chunks, { type: "video/mp4" });
|
|
7386
|
-
},
|
|
7387
|
-
state: () => recorder.state,
|
|
7388
|
-
dispose: () => {
|
|
7389
|
-
stream.getTracks().forEach((t) => t.stop());
|
|
6396
|
+
if (isEditable || mod) return;
|
|
6397
|
+
switch (event.key) {
|
|
6398
|
+
case " ":
|
|
6399
|
+
event.preventDefault();
|
|
6400
|
+
window.dispatchEvent(new CustomEvent("cut-test:toggle-play"));
|
|
6401
|
+
break;
|
|
6402
|
+
case "s":
|
|
6403
|
+
case "S": {
|
|
6404
|
+
event.preventDefault();
|
|
6405
|
+
const state = editor.store.getState();
|
|
6406
|
+
state.splitClipsAt(state.playheadUs, state.selectedClipIds.length > 0 ? state.selectedClipIds : void 0);
|
|
6407
|
+
break;
|
|
6408
|
+
}
|
|
6409
|
+
case "Delete":
|
|
6410
|
+
case "Backspace": {
|
|
6411
|
+
const state = editor.store.getState();
|
|
6412
|
+
if (state.selectedClipIds.length === 0) return;
|
|
6413
|
+
event.preventDefault();
|
|
6414
|
+
state.removeClips(state.selectedClipIds);
|
|
6415
|
+
break;
|
|
6416
|
+
}
|
|
6417
|
+
case "ArrowLeft":
|
|
6418
|
+
case "ArrowRight": {
|
|
6419
|
+
event.preventDefault();
|
|
6420
|
+
const direction = event.key === "ArrowLeft" ? -1 : 1;
|
|
6421
|
+
const state = editor.store.getState();
|
|
6422
|
+
const total = state.project.clips.reduce((max, c) => Math.max(max, c.startUs + c.durationUs), 0);
|
|
6423
|
+
const next = Math.max(0, Math.min(total, state.playheadUs + direction * FRAME_US));
|
|
6424
|
+
state.setPlayhead(next);
|
|
6425
|
+
window.dispatchEvent(new CustomEvent("cut-test:step-frame", { detail: { direction } }));
|
|
6426
|
+
break;
|
|
6427
|
+
}
|
|
6428
|
+
case "Home":
|
|
6429
|
+
event.preventDefault();
|
|
6430
|
+
editor.store.getState().setPlayhead(0);
|
|
6431
|
+
window.dispatchEvent(new CustomEvent("cut-test:seek", { detail: { us: 0 } }));
|
|
6432
|
+
break;
|
|
6433
|
+
case "End": {
|
|
6434
|
+
event.preventDefault();
|
|
6435
|
+
const state = editor.store.getState();
|
|
6436
|
+
const total = state.project.clips.reduce((max, c) => Math.max(max, c.startUs + c.durationUs), 0);
|
|
6437
|
+
state.setPlayhead(total);
|
|
6438
|
+
window.dispatchEvent(new CustomEvent("cut-test:seek", { detail: { us: total } }));
|
|
6439
|
+
break;
|
|
6440
|
+
}
|
|
6441
|
+
case "+":
|
|
6442
|
+
case "=": {
|
|
6443
|
+
event.preventDefault();
|
|
6444
|
+
const ui = uiApi.getState();
|
|
6445
|
+
ui.setPxPerSecond((0, _cut_workbench_core.clamp)(ui.pxPerSecond * 1.25, 10, 500));
|
|
6446
|
+
break;
|
|
6447
|
+
}
|
|
6448
|
+
case "-":
|
|
6449
|
+
case "_": {
|
|
6450
|
+
event.preventDefault();
|
|
6451
|
+
const ui = uiApi.getState();
|
|
6452
|
+
ui.setPxPerSecond((0, _cut_workbench_core.clamp)(ui.pxPerSecond / 1.25, 10, 500));
|
|
6453
|
+
break;
|
|
6454
|
+
}
|
|
6455
|
+
case "Escape": editor.store.getState().selectClips([]);
|
|
7390
6456
|
}
|
|
7391
|
-
};
|
|
6457
|
+
}, []);
|
|
6458
|
+
(0, react.useEffect)(() => {
|
|
6459
|
+
window.addEventListener("keydown", handler);
|
|
6460
|
+
return () => window.removeEventListener("keydown", handler);
|
|
6461
|
+
}, [handler]);
|
|
7392
6462
|
}
|
|
7393
|
-
// src/
|
|
6463
|
+
// src/hooks/use-capability.ts
|
|
6464
|
+
var cached = null;
|
|
7394
6465
|
/**
|
|
7395
|
-
*
|
|
7396
|
-
*
|
|
7397
|
-
* `FrameCounterClip` 是本项目实现的 IClip:不依赖任何媒体文件,
|
|
7398
|
-
* 逐帧用 OffscreenCanvas 画出帧号 / 进度条 / 色相变化。
|
|
6466
|
+
* 启动时探测一次浏览器能力(结果在会话内缓存)。
|
|
7399
6467
|
*
|
|
7400
|
-
*
|
|
7401
|
-
* 且能直接被 Combinator 编码成 MP4。
|
|
6468
|
+
* 必须在渲染任何 WebAV 组件前完成,否则 opfs-tools 的模块副作用可能直接抛错。
|
|
7402
6469
|
*/
|
|
7403
|
-
function
|
|
7404
|
-
const [
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
const clipRef = (0, react.useRef)(null);
|
|
7410
|
-
const rafRef = (0, react.useRef)(null);
|
|
7411
|
-
const { busy, run } = useLabAction();
|
|
7412
|
-
const durationUs = Math.round(durationSec * 1e6);
|
|
7413
|
-
(0, react.useEffect)(() => {
|
|
7414
|
-
clipRef.current?.destroy();
|
|
7415
|
-
try {
|
|
7416
|
-
clipRef.current = new _cut_workbench_core_lab.FrameCounterClip(640, 360, durationUs, 30);
|
|
7417
|
-
} catch (err) {
|
|
7418
|
-
console.warn("[CustomClipLab] create failed", err);
|
|
7419
|
-
clipRef.current = null;
|
|
7420
|
-
}
|
|
7421
|
-
return () => {
|
|
7422
|
-
clipRef.current?.destroy();
|
|
7423
|
-
clipRef.current = null;
|
|
7424
|
-
};
|
|
7425
|
-
}, [durationUs]);
|
|
7426
|
-
const draw = (0, react.useCallback)(async () => {
|
|
7427
|
-
const clip = clipRef.current;
|
|
7428
|
-
const canvas = canvasRef.current;
|
|
7429
|
-
if (clip == null || canvas == null) return;
|
|
7430
|
-
await (0, _cut_workbench_core_lab.renderClipFrameToCanvas)(clip, timeUs, canvas);
|
|
7431
|
-
}, [timeUs]);
|
|
7432
|
-
(0, react.useEffect)(() => {
|
|
7433
|
-
draw();
|
|
7434
|
-
}, [draw]);
|
|
6470
|
+
function useCapability() {
|
|
6471
|
+
const [state, setState] = (0, react.useState)({
|
|
6472
|
+
report: cached,
|
|
6473
|
+
loading: cached == null,
|
|
6474
|
+
error: null
|
|
6475
|
+
});
|
|
7435
6476
|
(0, react.useEffect)(() => {
|
|
7436
|
-
if (
|
|
7437
|
-
let
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
6477
|
+
if (cached != null) return;
|
|
6478
|
+
let cancelled = false;
|
|
6479
|
+
(0, _cut_workbench_core_engine.detectEnvironment)().then((report) => {
|
|
6480
|
+
cached = report;
|
|
6481
|
+
if (!cancelled) setState({
|
|
6482
|
+
report,
|
|
6483
|
+
loading: false,
|
|
6484
|
+
error: null
|
|
6485
|
+
});
|
|
6486
|
+
}).catch((err) => {
|
|
6487
|
+
if (!cancelled) setState({
|
|
6488
|
+
report: null,
|
|
6489
|
+
loading: false,
|
|
6490
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6491
|
+
});
|
|
6492
|
+
});
|
|
7452
6493
|
return () => {
|
|
7453
|
-
|
|
6494
|
+
cancelled = true;
|
|
7454
6495
|
};
|
|
7455
|
-
}, [
|
|
7456
|
-
|
|
7457
|
-
const clip = clipRef.current;
|
|
7458
|
-
if (clip == null) return;
|
|
7459
|
-
const output = await run("正在编码自定义 Clip…", () => (0, _cut_workbench_core_lab.exportCustomClip)(clip, durationUs));
|
|
7460
|
-
if (output != null) setResult(output);
|
|
7461
|
-
};
|
|
7462
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LabCard, {
|
|
7463
|
-
title: "自定义 IClip",
|
|
7464
|
-
description: "纯程序化渲染的 Clip(帧计数器),不依赖任何媒体文件,证明接口可扩展。",
|
|
7465
|
-
apis: [
|
|
7466
|
-
"IClip.tick",
|
|
7467
|
-
"IClip.clone",
|
|
7468
|
-
"IClip.split",
|
|
7469
|
-
"Combinator"
|
|
7470
|
-
],
|
|
7471
|
-
children: [
|
|
7472
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7473
|
-
className: "overflow-hidden rounded-md border bg-stage-bg",
|
|
7474
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
|
|
7475
|
-
ref: canvasRef,
|
|
7476
|
-
width: 640,
|
|
7477
|
-
height: 360,
|
|
7478
|
-
className: "block w-full",
|
|
7479
|
-
style: { maxHeight: 180 }
|
|
7480
|
-
})
|
|
7481
|
-
}),
|
|
7482
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7483
|
-
className: "flex items-center gap-2",
|
|
7484
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
7485
|
-
type: "button",
|
|
7486
|
-
onClick: () => {
|
|
7487
|
-
if (!playing && timeUs >= durationUs - 1) setTimeUs(0);
|
|
7488
|
-
setPlaying((p) => !p);
|
|
7489
|
-
},
|
|
7490
|
-
className: "flex items-center gap-1 rounded-md bg-secondary px-2 py-1 text-[11px]",
|
|
7491
|
-
children: [playing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Square, { className: "size-3" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Play, { className: "size-3" }), playing ? "停止" : "预览"]
|
|
7492
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
7493
|
-
className: "font-mono text-[10px] text-muted-foreground tabular-nums",
|
|
7494
|
-
children: [
|
|
7495
|
-
(0, _cut_workbench_core.formatTimecode)(timeUs),
|
|
7496
|
-
" / ",
|
|
7497
|
-
(0, _cut_workbench_core.formatTimecode)(durationUs)
|
|
7498
|
-
]
|
|
7499
|
-
})]
|
|
7500
|
-
}),
|
|
7501
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderRow, {
|
|
7502
|
-
label: "时间",
|
|
7503
|
-
value: timeUs,
|
|
7504
|
-
min: 0,
|
|
7505
|
-
max: Math.max(1, durationUs - 1),
|
|
7506
|
-
step: 1e4,
|
|
7507
|
-
format: (v) => `${(v / 1e6).toFixed(2)}s`,
|
|
7508
|
-
onChange: (v) => {
|
|
7509
|
-
setPlaying(false);
|
|
7510
|
-
setTimeUs(v);
|
|
7511
|
-
}
|
|
7512
|
-
}),
|
|
7513
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderRow, {
|
|
7514
|
-
label: "时长",
|
|
7515
|
-
value: durationSec,
|
|
7516
|
-
min: 1,
|
|
7517
|
-
max: 20,
|
|
7518
|
-
step: .5,
|
|
7519
|
-
format: (v) => `${v.toFixed(1)}s`,
|
|
7520
|
-
onChange: (v) => {
|
|
7521
|
-
setPlaying(false);
|
|
7522
|
-
setDurationSec(v);
|
|
7523
|
-
setTimeUs(0);
|
|
7524
|
-
}
|
|
7525
|
-
}),
|
|
7526
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7527
|
-
type: "button",
|
|
7528
|
-
disabled: busy != null,
|
|
7529
|
-
onClick: () => void handleExport(),
|
|
7530
|
-
className: "rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground disabled:opacity-50",
|
|
7531
|
-
children: "编码为 MP4"
|
|
7532
|
-
}),
|
|
7533
|
-
busy != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Busy, { label: busy }),
|
|
7534
|
-
result != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResultPreview, {
|
|
7535
|
-
blob: result,
|
|
7536
|
-
filename: "custom-clip.mp4"
|
|
7537
|
-
})
|
|
7538
|
-
]
|
|
7539
|
-
});
|
|
6496
|
+
}, []);
|
|
6497
|
+
return state;
|
|
7540
6498
|
}
|
|
7541
|
-
// src/
|
|
7542
|
-
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7560
|
-
className: "text-xs text-muted-foreground",
|
|
7561
|
-
children: "覆盖 av-cliper / av-canvas / av-recorder 的进阶 API。每个卡片可独立运行,失败时给出可读原因。"
|
|
7562
|
-
})]
|
|
7563
|
-
}),
|
|
7564
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7565
|
-
className: "grid gap-3 md:grid-cols-2 xl:grid-cols-3",
|
|
7566
|
-
children: [
|
|
7567
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConcatenateLab, {}),
|
|
7568
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RemuxLab, {}),
|
|
7569
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChromakeyLab, {}),
|
|
7570
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SubtitleLab, {}),
|
|
7571
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrackSplitLab, {}),
|
|
7572
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThumbnailLab, {}),
|
|
7573
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecorderLab, {}),
|
|
7574
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomClipLab, {})
|
|
7575
|
-
]
|
|
7576
|
-
}),
|
|
7577
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("footer", {
|
|
7578
|
-
className: "border-t pt-3 text-[10px] leading-relaxed text-muted-foreground",
|
|
7579
|
-
children: "说明:所有输出均为 MP4(H.264 + AAC 48kHz 双声道)。WebAV 生成的是 fMP4 分片, 部分播放器需「修复时长」;浏览器本地处理,文件不会上传。"
|
|
7580
|
-
})
|
|
7581
|
-
]
|
|
7582
|
-
});
|
|
6499
|
+
// src/components/layout/use-capability-info.ts
|
|
6500
|
+
function deriveStatus(report, loading) {
|
|
6501
|
+
if (loading) return "checking";
|
|
6502
|
+
if (report == null) return "unsupported";
|
|
6503
|
+
if (report.opfs && report.composerSupported && report.issues.length === 0) return "ready";
|
|
6504
|
+
if (report.opfs && report.composerSupported) return "degraded";
|
|
6505
|
+
return "unsupported";
|
|
6506
|
+
}
|
|
6507
|
+
/** 能力探测的 UI 适配层:把 core 的报告转成状态与文案 */
|
|
6508
|
+
function useCapabilityInfo() {
|
|
6509
|
+
const { report, loading, error } = useCapability();
|
|
6510
|
+
return {
|
|
6511
|
+
report,
|
|
6512
|
+
loading,
|
|
6513
|
+
error,
|
|
6514
|
+
status: deriveStatus(report, loading),
|
|
6515
|
+
summary: (0, _cut_workbench_core_engine.summarizeCapabilities)(report)
|
|
6516
|
+
};
|
|
7583
6517
|
}
|
|
7584
6518
|
// src/CutWorkbench.tsx
|
|
7585
6519
|
/**
|
|
@@ -7592,24 +6526,35 @@ function LabPage() {
|
|
|
7592
6526
|
* - 不强制能力阻断页(宿主可自行决定)。
|
|
7593
6527
|
*/
|
|
7594
6528
|
/**
|
|
6529
|
+
* Lab 的懒加载器。
|
|
6530
|
+
*
|
|
6531
|
+
* ⚠️ 用**字面量**模块说明符做动态 import,而不是先从 `./features/lab` 拿组件:
|
|
6532
|
+
* 前者会被打包成 external(`import('@cut-workbench/workbench/lab')`),由宿主
|
|
6533
|
+
* 打包器解析;后者会把 lab 整棵子树(含 `@webav/av-recorder`)拉回本包的模块图。
|
|
6534
|
+
*
|
|
6535
|
+
* 解析到 lab 还是 stub,由宿主的 `resolve.alias` 决定 —— 这是移除 lab 的
|
|
6536
|
+
* 唯一可靠方式(运行时开关删不掉已产出的 chunk,见 src/lab.ts 顶部说明)。
|
|
6537
|
+
*/
|
|
6538
|
+
var LabPage = (0, react.lazy)(() => import("@cut-workbench/workbench/lab").then((m) => ({ default: m.LabPage })));
|
|
6539
|
+
/**
|
|
7595
6540
|
* 工作台主体。
|
|
7596
6541
|
*
|
|
7597
6542
|
* 必须在 `<WorkbenchProvider>` + `<CutEditorProvider>` 之内使用。
|
|
7598
6543
|
*/
|
|
7599
|
-
function CutWorkbench({ slots, className, onSaveProject, onOpenProject, enforceCapability = true }) {
|
|
6544
|
+
function CutWorkbench({ slots, toolbar: toolbarOptions, className, onSaveProject, onOpenProject, enforceCapability = true }) {
|
|
7600
6545
|
const editor = (0, _cut_workbench_react.useCutEditor)();
|
|
7601
6546
|
const capability = useCapabilityInfo();
|
|
7602
|
-
const leftWidth = useUi((s) => s.leftWidth);
|
|
7603
|
-
const rightWidth = useUi((s) => s.rightWidth);
|
|
7604
|
-
const timelineHeight = useUi((s) => s.timelineHeight);
|
|
7605
|
-
const setLeftWidth = useUi((s) => s.setLeftWidth);
|
|
7606
|
-
const setRightWidth = useUi((s) => s.setRightWidth);
|
|
7607
|
-
const setTimelineHeight = useUi((s) => s.setTimelineHeight);
|
|
7608
|
-
const openDialog = useUi((s) => s.openDialog);
|
|
7609
|
-
const closeDialog = useUi((s) => s.closeDialog);
|
|
7610
|
-
const dialog = useUi((s) => s.dialog);
|
|
7611
|
-
const { config, theme } = useWorkbench();
|
|
7612
|
-
const route = useUi((s) => s.route);
|
|
6547
|
+
const leftWidth = require_notify.useUi((s) => s.leftWidth);
|
|
6548
|
+
const rightWidth = require_notify.useUi((s) => s.rightWidth);
|
|
6549
|
+
const timelineHeight = require_notify.useUi((s) => s.timelineHeight);
|
|
6550
|
+
const setLeftWidth = require_notify.useUi((s) => s.setLeftWidth);
|
|
6551
|
+
const setRightWidth = require_notify.useUi((s) => s.setRightWidth);
|
|
6552
|
+
const setTimelineHeight = require_notify.useUi((s) => s.setTimelineHeight);
|
|
6553
|
+
const openDialog = require_notify.useUi((s) => s.openDialog);
|
|
6554
|
+
const closeDialog = require_notify.useUi((s) => s.closeDialog);
|
|
6555
|
+
const dialog = require_notify.useUi((s) => s.dialog);
|
|
6556
|
+
const { config, theme } = require_notify.useWorkbench();
|
|
6557
|
+
const route = require_notify.useUi((s) => s.route);
|
|
7613
6558
|
const addToTimeline = useAddToTimeline();
|
|
7614
6559
|
const removeAsset = (0, _cut_workbench_react.useEditor)((s) => s.removeAsset);
|
|
7615
6560
|
const [busy, setBusy] = (0, react.useState)(null);
|
|
@@ -7660,10 +6605,18 @@ function CutWorkbench({ slots, className, onSaveProject, onOpenProject, enforceC
|
|
|
7660
6605
|
onExport: handleExport,
|
|
7661
6606
|
onSaveProject: onSaveProject != null ? handleSave : void 0,
|
|
7662
6607
|
onOpenProject: onOpenProject != null ? handleOpen : void 0,
|
|
6608
|
+
brand: toolbarOptions?.brand,
|
|
6609
|
+
title: toolbarOptions?.title,
|
|
6610
|
+
status: toolbarOptions?.status,
|
|
6611
|
+
actions: toolbarOptions?.actions,
|
|
6612
|
+
showCapabilityBadge: toolbarOptions?.showCapabilityBadge,
|
|
6613
|
+
editableTitle: toolbarOptions?.editableTitle,
|
|
6614
|
+
labels: toolbarOptions?.labels,
|
|
7663
6615
|
showLab: config.features.lab
|
|
7664
6616
|
});
|
|
7665
6617
|
}, [
|
|
7666
6618
|
slots?.topBar,
|
|
6619
|
+
toolbarOptions,
|
|
7667
6620
|
capability,
|
|
7668
6621
|
handleExport,
|
|
7669
6622
|
handleSave,
|
|
@@ -7690,7 +6643,17 @@ function CutWorkbench({ slots, className, onSaveProject, onOpenProject, enforceC
|
|
|
7690
6643
|
toolbar,
|
|
7691
6644
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7692
6645
|
className: "min-h-0 flex-1 overflow-auto",
|
|
7693
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
6646
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.Suspense, {
|
|
6647
|
+
fallback: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6648
|
+
className: "flex h-full items-center justify-center",
|
|
6649
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoadingOverlay, {
|
|
6650
|
+
visible: true,
|
|
6651
|
+
title: "正在加载能力实验室…",
|
|
6652
|
+
fullscreen: false
|
|
6653
|
+
})
|
|
6654
|
+
}),
|
|
6655
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LabPage, {})
|
|
6656
|
+
})
|
|
7694
6657
|
}),
|
|
7695
6658
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExportDialog, {}),
|
|
7696
6659
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CanvasSettingsDialog, {
|
|
@@ -8638,11 +7601,15 @@ function CutWorkbenchProvider({ children, bridge, theme = "dark", tokens, config
|
|
|
8638
7601
|
(0, _cut_workbench_react.useCutEditor)();
|
|
8639
7602
|
const [ui] = (0, react.useState)(() => createUiStore());
|
|
8640
7603
|
const resolvedBridge = (0, react.useMemo)(() => bridge ?? createDefaultBridge(), [bridge]);
|
|
7604
|
+
(0, react.useEffect)(() => {
|
|
7605
|
+
require_notify.setDefaultBridge(resolvedBridge);
|
|
7606
|
+
return () => require_notify.setDefaultBridge(null);
|
|
7607
|
+
}, [resolvedBridge]);
|
|
8641
7608
|
const mergedConfig = (0, react.useMemo)(() => ({
|
|
8642
|
-
...DEFAULT_CONFIG,
|
|
7609
|
+
...require_notify.DEFAULT_CONFIG,
|
|
8643
7610
|
...config,
|
|
8644
7611
|
features: {
|
|
8645
|
-
...DEFAULT_CONFIG.features,
|
|
7612
|
+
...require_notify.DEFAULT_CONFIG.features,
|
|
8646
7613
|
...config?.features ?? {}
|
|
8647
7614
|
}
|
|
8648
7615
|
}), [config]);
|
|
@@ -8671,7 +7638,7 @@ function CutWorkbenchProvider({ children, bridge, theme = "dark", tokens, config
|
|
|
8671
7638
|
resolvedTheme,
|
|
8672
7639
|
portalContainer
|
|
8673
7640
|
]);
|
|
8674
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkbenchContext.Provider, {
|
|
7641
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_notify.WorkbenchContext.Provider, {
|
|
8675
7642
|
value,
|
|
8676
7643
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PortalContainerProvider, {
|
|
8677
7644
|
container: portalContainer,
|
|
@@ -8768,6 +7735,7 @@ function initCutWorkbench(container, options = {}) {
|
|
|
8768
7735
|
}, (0, react.createElement)(CutWorkbench, {
|
|
8769
7736
|
className: options.className ?? "h-full w-full",
|
|
8770
7737
|
enforceCapability: options.enforceCapability ?? true,
|
|
7738
|
+
toolbar: options.toolbar,
|
|
8771
7739
|
onSaveProject: () => void instance.saveProject(),
|
|
8772
7740
|
onOpenProject: async () => {
|
|
8773
7741
|
const saved = await storage.load(namespace);
|
|
@@ -8923,8 +7891,8 @@ exports.ClipBlock = ClipBlock;
|
|
|
8923
7891
|
exports.ClipContextMenu = ClipContextMenu;
|
|
8924
7892
|
exports.CutWorkbench = CutWorkbench;
|
|
8925
7893
|
exports.CutWorkbenchProvider = CutWorkbenchProvider;
|
|
8926
|
-
exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
|
|
8927
|
-
exports.DEFAULT_FEATURES = DEFAULT_FEATURES;
|
|
7894
|
+
exports.DEFAULT_CONFIG = require_notify.DEFAULT_CONFIG;
|
|
7895
|
+
exports.DEFAULT_FEATURES = require_notify.DEFAULT_FEATURES;
|
|
8928
7896
|
exports.DropZone = DropZone;
|
|
8929
7897
|
exports.EmptyState = EmptyState;
|
|
8930
7898
|
exports.ExportDialog = ExportDialog;
|
|
@@ -8972,8 +7940,8 @@ exports.useExport = useExport;
|
|
|
8972
7940
|
exports.useHostDarkClass = useHostDarkClass;
|
|
8973
7941
|
exports.useMediaImport = useMediaImport;
|
|
8974
7942
|
exports.usePortalContainer = usePortalContainer;
|
|
8975
|
-
exports.useUi = useUi;
|
|
8976
|
-
exports.useUiStoreApi = useUiStoreApi;
|
|
8977
|
-
exports.useWorkbench = useWorkbench;
|
|
7943
|
+
exports.useUi = require_notify.useUi;
|
|
7944
|
+
exports.useUiStoreApi = require_notify.useUiStoreApi;
|
|
7945
|
+
exports.useWorkbench = require_notify.useWorkbench;
|
|
8978
7946
|
|
|
8979
7947
|
//# sourceMappingURL=index.cjs.map
|