@cut-workbench/workbench 0.1.0 → 0.2.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/format--DhqHMgX.cjs +208 -0
- package/dist/format--DhqHMgX.cjs.map +1 -0
- package/dist/format-CV2aoZZC.js +109 -0
- package/dist/format-CV2aoZZC.js.map +1 -0
- package/dist/index.cjs +500 -1620
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +419 -1518
- 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 +1178 -0
- package/dist/lab.cjs.map +1 -0
- package/dist/lab.d.ts +32 -0
- package/dist/lab.js +1177 -0
- package/dist/lab.js.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_format = require("./format--DhqHMgX.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_format.__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_format.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_format.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_format.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_format.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_format.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_format.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]
|
|
@@ -387,10 +352,17 @@ var DEFAULT_CONFIG = {
|
|
|
387
352
|
* - 品牌区可替换(`brand`),不强制显示项目名;
|
|
388
353
|
* - 保存/打开是可选回调,宿主不提供就不渲染按钮;
|
|
389
354
|
* - 「视图切换(编辑器/实验室)」只在开启 lab 特性时显示。
|
|
355
|
+
*
|
|
356
|
+
* 可自定义的层次(由浅到深):
|
|
357
|
+
* 1. **文案**:`labels` 覆盖工程名占位、能力徽标等;
|
|
358
|
+
* 2. **局部**:`brand` / `title` / `status` / `actions` / `showCapabilityBadge`
|
|
359
|
+
* 逐块替换;
|
|
360
|
+
* 3. **整条**:`slots.topBar`(传 `null` 表示完全不要顶栏)。
|
|
390
361
|
*/
|
|
391
362
|
/** 顶栏:品牌 / 工程名 / 撤销重做 / 保存 / 导出 / 能力徽标 / 视图切换 */
|
|
392
|
-
function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject, brand, showLab = false }) {
|
|
363
|
+
function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject, brand, title, status, actions, showCapabilityBadge = true, showLab = false, editableTitle = false, labels }) {
|
|
393
364
|
const projectName = (0, _cut_workbench_react.useEditor)((s) => s.project.name);
|
|
365
|
+
const renameProject = (0, _cut_workbench_react.useEditor)((s) => s.renameProject);
|
|
394
366
|
const dirty = (0, _cut_workbench_react.useEditor)((s) => s.dirty);
|
|
395
367
|
const undo = (0, _cut_workbench_react.useEditor)((s) => s.undo);
|
|
396
368
|
const redo = (0, _cut_workbench_react.useEditor)((s) => s.redo);
|
|
@@ -402,44 +374,49 @@ function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject,
|
|
|
402
374
|
const clipCount = (0, _cut_workbench_react.useEditor)((s) => s.project.clips.length);
|
|
403
375
|
const projectWidth = (0, _cut_workbench_react.useEditor)((s) => s.project.width);
|
|
404
376
|
const projectHeight = (0, _cut_workbench_react.useEditor)((s) => s.project.height);
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
377
|
+
const text = {
|
|
378
|
+
untitledProject: "未命名工程",
|
|
379
|
+
emptyProject: "空工程",
|
|
380
|
+
export: "导出",
|
|
381
|
+
canvasSettings: "画布设置",
|
|
382
|
+
showLab: "能力实验室",
|
|
383
|
+
backToEditor: "返回编辑器",
|
|
384
|
+
save: "保存工程",
|
|
385
|
+
open: "加载工程",
|
|
386
|
+
undo: "撤销",
|
|
387
|
+
redo: "重做",
|
|
388
|
+
...labels
|
|
389
|
+
};
|
|
390
|
+
const branding = brand == null ? null : brand;
|
|
391
|
+
const projectTitle = title === null ? null : title ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProjectTitle, {
|
|
392
|
+
name: projectName,
|
|
393
|
+
dirty,
|
|
394
|
+
placeholder: text.untitledProject,
|
|
395
|
+
editable: editableTitle,
|
|
396
|
+
onRename: renameProject
|
|
397
|
+
});
|
|
398
|
+
const statusNode = status === null ? null : status ?? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
399
|
+
className: "hidden text-xs text-muted-foreground sm:inline",
|
|
400
|
+
children: [
|
|
401
|
+
clipCount > 0 ? `${clipCount} 个片段` : text.emptyProject,
|
|
402
|
+
" ·",
|
|
403
|
+
" ",
|
|
404
|
+
(0, _cut_workbench_core.ratioLabel)(projectWidth, projectHeight)
|
|
405
|
+
]
|
|
414
406
|
});
|
|
415
407
|
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",
|
|
408
|
+
className: "flex h-12 shrink-0 items-center gap-2 overflow-hidden border-b bg-card px-3",
|
|
409
|
+
"data-workbench-toolbar": true,
|
|
417
410
|
children: [
|
|
418
411
|
branding,
|
|
419
|
-
branding != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
|
|
412
|
+
branding != null && projectTitle != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
|
|
420
413
|
orientation: "vertical",
|
|
421
414
|
className: "h-5"
|
|
422
415
|
}),
|
|
416
|
+
projectTitle,
|
|
417
|
+
statusNode,
|
|
423
418
|
/* @__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",
|
|
419
|
+
className: "ml-auto flex shrink-0 items-center gap-1",
|
|
443
420
|
children: [
|
|
444
421
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
445
422
|
asChild: true,
|
|
@@ -448,10 +425,10 @@ function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject,
|
|
|
448
425
|
size: "icon-sm",
|
|
449
426
|
onClick: undo,
|
|
450
427
|
disabled: !canUndo,
|
|
451
|
-
"aria-label":
|
|
428
|
+
"aria-label": text.undo,
|
|
452
429
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Undo2, { className: "size-4" })
|
|
453
430
|
})
|
|
454
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
431
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(TooltipContent, { children: [text.undo, " (Ctrl+Z)"] })] }),
|
|
455
432
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
456
433
|
asChild: true,
|
|
457
434
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
@@ -459,10 +436,10 @@ function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject,
|
|
|
459
436
|
size: "icon-sm",
|
|
460
437
|
onClick: redo,
|
|
461
438
|
disabled: !canRedo,
|
|
462
|
-
"aria-label":
|
|
439
|
+
"aria-label": text.redo,
|
|
463
440
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Redo2, { className: "size-4" })
|
|
464
441
|
})
|
|
465
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
442
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(TooltipContent, { children: [text.redo, " (Ctrl+Shift+Z)"] })] }),
|
|
466
443
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
|
|
467
444
|
orientation: "vertical",
|
|
468
445
|
className: "mx-1 h-5"
|
|
@@ -473,29 +450,29 @@ function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject,
|
|
|
473
450
|
variant: "ghost",
|
|
474
451
|
size: "icon-sm",
|
|
475
452
|
onClick: onOpenProject,
|
|
476
|
-
"aria-label":
|
|
453
|
+
"aria-label": text.open,
|
|
477
454
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.FolderOpen, { className: "size-4" })
|
|
478
455
|
})
|
|
479
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children:
|
|
456
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: text.open })] }),
|
|
480
457
|
onSaveProject != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
481
458
|
asChild: true,
|
|
482
459
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
483
460
|
variant: "ghost",
|
|
484
461
|
size: "icon-sm",
|
|
485
462
|
onClick: onSaveProject,
|
|
486
|
-
"aria-label":
|
|
463
|
+
"aria-label": text.save,
|
|
487
464
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Save, { className: "size-4" })
|
|
488
465
|
})
|
|
489
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children:
|
|
466
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: text.save })] }),
|
|
490
467
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
|
|
491
468
|
variant: "ghost",
|
|
492
469
|
size: "sm",
|
|
493
470
|
className: "gap-1.5",
|
|
494
|
-
"aria-label":
|
|
471
|
+
"aria-label": text.canvasSettings,
|
|
495
472
|
onClick: () => openDialog("projectSettings"),
|
|
496
473
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Monitor, { className: "size-4" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
497
474
|
className: "hidden md:inline",
|
|
498
|
-
children:
|
|
475
|
+
children: text.canvasSettings
|
|
499
476
|
})]
|
|
500
477
|
}),
|
|
501
478
|
showLab && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
|
|
@@ -505,21 +482,67 @@ function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject,
|
|
|
505
482
|
onClick: () => setRoute(route === "lab" ? "editor" : "lab"),
|
|
506
483
|
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
484
|
className: "hidden md:inline",
|
|
508
|
-
children: route === "lab" ?
|
|
485
|
+
children: route === "lab" ? text.backToEditor : text.showLab
|
|
509
486
|
})]
|
|
510
487
|
}),
|
|
511
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapabilityBadge, { info: capability }),
|
|
488
|
+
showCapabilityBadge && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapabilityBadge, { info: capability }),
|
|
489
|
+
actions,
|
|
512
490
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
|
|
513
491
|
size: "sm",
|
|
514
492
|
className: "ml-1 gap-1.5",
|
|
515
493
|
onClick: onExport,
|
|
516
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Download, { className: "size-4" }),
|
|
494
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Download, { className: "size-4" }), text.export]
|
|
517
495
|
})
|
|
518
496
|
]
|
|
519
497
|
})
|
|
520
498
|
]
|
|
521
499
|
});
|
|
522
500
|
}
|
|
501
|
+
/**
|
|
502
|
+
* 工程名展示(可选双击重命名)。
|
|
503
|
+
*
|
|
504
|
+
* 重命名走编辑器 store 的 `renameProject`,因此会进撤销栈、并标记 dirty ——
|
|
505
|
+
* 直接改 DOM 或本地 state 会让「保存了什么」和「看到什么」对不上。
|
|
506
|
+
*/
|
|
507
|
+
function ProjectTitle({ name, dirty, placeholder, editable, onRename }) {
|
|
508
|
+
const [editing, setEditing] = (0, react.useState)(false);
|
|
509
|
+
const [draft, setDraft] = (0, react.useState)(name);
|
|
510
|
+
const commit = () => {
|
|
511
|
+
setEditing(false);
|
|
512
|
+
const next = draft.trim();
|
|
513
|
+
if (next.length > 0 && next !== name) onRename(next);
|
|
514
|
+
};
|
|
515
|
+
if (editing) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
516
|
+
autoFocus: true,
|
|
517
|
+
value: draft,
|
|
518
|
+
onChange: (e) => setDraft(e.target.value),
|
|
519
|
+
onBlur: commit,
|
|
520
|
+
onKeyDown: (e) => {
|
|
521
|
+
if (e.key === "Enter") commit();
|
|
522
|
+
if (e.key === "Escape") {
|
|
523
|
+
setDraft(name);
|
|
524
|
+
setEditing(false);
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
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",
|
|
528
|
+
"aria-label": "工程名称"
|
|
529
|
+
});
|
|
530
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
531
|
+
className: "flex min-w-0 items-center gap-1.5",
|
|
532
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
533
|
+
className: require_format.cn("truncate text-sm font-medium", editable && "cursor-text"),
|
|
534
|
+
title: editable ? "双击重命名" : name.length > 0 ? name : placeholder,
|
|
535
|
+
onDoubleClick: editable ? () => {
|
|
536
|
+
setDraft(name.length > 0 ? name : "");
|
|
537
|
+
setEditing(true);
|
|
538
|
+
} : void 0,
|
|
539
|
+
children: name.length > 0 ? name : placeholder
|
|
540
|
+
}), dirty && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
541
|
+
className: "size-1.5 shrink-0 rounded-full bg-primary",
|
|
542
|
+
title: "未保存"
|
|
543
|
+
})]
|
|
544
|
+
});
|
|
545
|
+
}
|
|
523
546
|
// src/components/layout/use-panel-resize.ts
|
|
524
547
|
/**
|
|
525
548
|
* 面板拖拽调整。内部用 rAF 节流,松手才把最终值写回 store(避免每帧重渲染整棵树)。
|
|
@@ -598,44 +621,8 @@ function PanelResizer({ orientation, value, min, max, invert, onChange, classNam
|
|
|
598
621
|
"aria-orientation": isVertical ? "vertical" : "horizontal",
|
|
599
622
|
"aria-label": label,
|
|
600
623
|
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
|
|
624
|
+
className: require_format.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),
|
|
625
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: require_format.cn("absolute", isVertical ? "-left-1 -right-1 inset-y-0" : "-top-1 -bottom-1 inset-x-0") })
|
|
639
626
|
});
|
|
640
627
|
}
|
|
641
628
|
// src/components/common/LoadingOverlay.tsx
|
|
@@ -643,7 +630,7 @@ function CardContent({ className, ...props }) {
|
|
|
643
630
|
function LoadingOverlay({ visible, title = "处理中…", description, progress, className, fullscreen, children }) {
|
|
644
631
|
if (!visible) return null;
|
|
645
632
|
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),
|
|
633
|
+
className: require_format.cn("z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm", fullscreen ? "fixed inset-0" : "absolute inset-0", className),
|
|
647
634
|
role: "status",
|
|
648
635
|
"aria-live": "polite",
|
|
649
636
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -705,7 +692,12 @@ var CHECKS = [
|
|
|
705
692
|
hint: "绿幕抠像(可选)"
|
|
706
693
|
}
|
|
707
694
|
];
|
|
708
|
-
/**
|
|
695
|
+
/**
|
|
696
|
+
* 不满足运行条件时的阻断页:逐项说明缺失能力,不做静默降级。
|
|
697
|
+
*
|
|
698
|
+
* 「依赖没装」与「浏览器不支持」要分开呈现 —— 前者是宿主的构建问题(可执行
|
|
699
|
+
* 安装命令解决),后者只能换浏览器。混在一起会让人对着一条装包报错去升浏览器。
|
|
700
|
+
*/
|
|
709
701
|
function UnsupportedBrowser({ report, loading, error }) {
|
|
710
702
|
if (loading) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
711
703
|
className: "relative flex min-h-screen items-center justify-center bg-background",
|
|
@@ -715,23 +707,46 @@ function UnsupportedBrowser({ report, loading, error }) {
|
|
|
715
707
|
fullscreen: false
|
|
716
708
|
})
|
|
717
709
|
});
|
|
710
|
+
const missing = report?.missingDependencies ?? [];
|
|
711
|
+
const dependenciesMissing = missing.length > 0;
|
|
718
712
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
719
713
|
className: "flex min-h-screen items-center justify-center bg-background p-6",
|
|
720
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
|
|
714
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_format.Card, {
|
|
721
715
|
className: "w-full max-w-2xl",
|
|
722
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CardHeader, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
716
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_format.CardHeader, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
723
717
|
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, {
|
|
718
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.AlertOctagon, { className: "size-5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_format.CardTitle, {
|
|
725
719
|
className: "text-base",
|
|
726
|
-
children: "当前浏览器无法运行本应用"
|
|
720
|
+
children: dependenciesMissing ? "缺少引擎依赖" : "当前浏览器无法运行本应用"
|
|
727
721
|
})]
|
|
728
722
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
729
723
|
className: "text-sm text-muted-foreground",
|
|
730
|
-
children: "WebAV 依赖 WebCodecs 与 OPFS,仅支持 Chromium 内核浏览器。请使用最新版 Chrome 或 Edge(非无痕模式)重新打开。"
|
|
731
|
-
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CardContent, {
|
|
724
|
+
children: dependenciesMissing ? "工作台的预览与导出依赖 WebAV(@webav/av-cliper、@webav/av-canvas)。当前项目里没有解析到它们。" : "WebAV 依赖 WebCodecs 与 OPFS,仅支持 Chromium 内核浏览器。请使用最新版 Chrome 或 Edge(非无痕模式)重新打开。"
|
|
725
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_format.CardContent, {
|
|
732
726
|
className: "space-y-4",
|
|
733
727
|
children: [
|
|
734
|
-
|
|
728
|
+
dependenciesMissing && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
729
|
+
className: "space-y-2 rounded-md border border-destructive/40 bg-destructive/5 p-3",
|
|
730
|
+
children: [
|
|
731
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
732
|
+
className: "text-xs font-medium text-destructive",
|
|
733
|
+
children: ["未解析到:", missing.join("、")]
|
|
734
|
+
}),
|
|
735
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
736
|
+
className: "text-xs text-muted-foreground",
|
|
737
|
+
children: "在宿主项目里执行:"
|
|
738
|
+
}),
|
|
739
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
740
|
+
className: "overflow-x-auto rounded bg-muted px-3 py-2 text-xs",
|
|
741
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: _cut_workbench_core_engine.ENGINE_DEPENDENCIES_INSTALL_COMMAND })
|
|
742
|
+
}),
|
|
743
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
744
|
+
className: "text-xs text-muted-foreground",
|
|
745
|
+
children: "安装后需要重新跑一次构建 —— 缺失依赖只在构建期解析,浏览器里刷新页面没用。"
|
|
746
|
+
})
|
|
747
|
+
]
|
|
748
|
+
}),
|
|
749
|
+
report != null && !dependenciesMissing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
735
750
|
className: "grid gap-2 sm:grid-cols-2",
|
|
736
751
|
children: CHECKS.map((check) => {
|
|
737
752
|
const value = report[check.key];
|
|
@@ -801,7 +816,7 @@ var Toaster = (props) => {
|
|
|
801
816
|
function Textarea({ className, ...props }) {
|
|
802
817
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
803
818
|
"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),
|
|
819
|
+
className: require_format.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
820
|
...props
|
|
806
821
|
});
|
|
807
822
|
}
|
|
@@ -809,7 +824,7 @@ function Textarea({ className, ...props }) {
|
|
|
809
824
|
function Label({ className, ...props }) {
|
|
810
825
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Label.Root, {
|
|
811
826
|
"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),
|
|
827
|
+
className: require_format.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
828
|
...props
|
|
814
829
|
});
|
|
815
830
|
}
|
|
@@ -817,7 +832,7 @@ function Label({ className, ...props }) {
|
|
|
817
832
|
function ScrollArea({ className, children, ...props }) {
|
|
818
833
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.ScrollArea.Root, {
|
|
819
834
|
"data-slot": "scroll-area",
|
|
820
|
-
className: cn("relative", className),
|
|
835
|
+
className: require_format.cn("relative", className),
|
|
821
836
|
...props,
|
|
822
837
|
children: [
|
|
823
838
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ScrollArea.Viewport, {
|
|
@@ -834,7 +849,7 @@ function ScrollBar({ className, orientation = "vertical", ...props }) {
|
|
|
834
849
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ScrollArea.ScrollAreaScrollbar, {
|
|
835
850
|
"data-slot": "scroll-area-scrollbar",
|
|
836
851
|
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),
|
|
852
|
+
className: require_format.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
853
|
...props,
|
|
839
854
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ScrollArea.ScrollAreaThumb, {
|
|
840
855
|
"data-slot": "scroll-area-thumb",
|
|
@@ -873,7 +888,7 @@ function DialogPortal({ ...props }) {
|
|
|
873
888
|
function DialogOverlay({ className, ...props }) {
|
|
874
889
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Dialog.Overlay, {
|
|
875
890
|
"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),
|
|
891
|
+
className: require_format.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
892
|
...props
|
|
878
893
|
});
|
|
879
894
|
}
|
|
@@ -882,7 +897,7 @@ function DialogContent({ className, children, showCloseButton = true, ...props }
|
|
|
882
897
|
"data-slot": "dialog-portal",
|
|
883
898
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogOverlay, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Dialog.Content, {
|
|
884
899
|
"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),
|
|
900
|
+
className: require_format.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
901
|
...props,
|
|
887
902
|
children: [children, showCloseButton && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Dialog.Close, {
|
|
888
903
|
"data-slot": "dialog-close",
|
|
@@ -898,14 +913,14 @@ function DialogContent({ className, children, showCloseButton = true, ...props }
|
|
|
898
913
|
function DialogHeader({ className, ...props }) {
|
|
899
914
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
900
915
|
"data-slot": "dialog-header",
|
|
901
|
-
className: cn("flex flex-col gap-2 text-center sm:text-left", className),
|
|
916
|
+
className: require_format.cn("flex flex-col gap-2 text-center sm:text-left", className),
|
|
902
917
|
...props
|
|
903
918
|
});
|
|
904
919
|
}
|
|
905
920
|
function DialogFooter({ className, showCloseButton = false, children, ...props }) {
|
|
906
921
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
907
922
|
"data-slot": "dialog-footer",
|
|
908
|
-
className: cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className),
|
|
923
|
+
className: require_format.cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className),
|
|
909
924
|
...props,
|
|
910
925
|
children: [children, showCloseButton && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Dialog.Close, {
|
|
911
926
|
asChild: true,
|
|
@@ -919,14 +934,14 @@ function DialogFooter({ className, showCloseButton = false, children, ...props }
|
|
|
919
934
|
function DialogTitle({ className, ...props }) {
|
|
920
935
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Dialog.Title, {
|
|
921
936
|
"data-slot": "dialog-title",
|
|
922
|
-
className: cn("text-lg leading-none font-semibold", className),
|
|
937
|
+
className: require_format.cn("text-lg leading-none font-semibold", className),
|
|
923
938
|
...props
|
|
924
939
|
});
|
|
925
940
|
}
|
|
926
941
|
function DialogDescription({ className, ...props }) {
|
|
927
942
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Dialog.Description, {
|
|
928
943
|
"data-slot": "dialog-description",
|
|
929
|
-
className: cn("text-sm text-muted-foreground", className),
|
|
944
|
+
className: require_format.cn("text-sm text-muted-foreground", className),
|
|
930
945
|
...props
|
|
931
946
|
});
|
|
932
947
|
}
|
|
@@ -955,7 +970,7 @@ function DropdownMenuContent({ className, sideOffset = 4, ...props }) {
|
|
|
955
970
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuPortal, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.DropdownMenu.Content, {
|
|
956
971
|
"data-slot": "dropdown-menu-content",
|
|
957
972
|
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),
|
|
973
|
+
className: require_format.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
974
|
...props
|
|
960
975
|
}) });
|
|
961
976
|
}
|
|
@@ -964,7 +979,7 @@ function DropdownMenuItem({ className, inset, variant = "default", ...props }) {
|
|
|
964
979
|
"data-slot": "dropdown-menu-item",
|
|
965
980
|
"data-inset": inset,
|
|
966
981
|
"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),
|
|
982
|
+
className: require_format.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
983
|
...props
|
|
969
984
|
});
|
|
970
985
|
}
|
|
@@ -972,14 +987,14 @@ function DropdownMenuLabel({ className, inset, ...props }) {
|
|
|
972
987
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.DropdownMenu.Label, {
|
|
973
988
|
"data-slot": "dropdown-menu-label",
|
|
974
989
|
"data-inset": inset,
|
|
975
|
-
className: cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className),
|
|
990
|
+
className: require_format.cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className),
|
|
976
991
|
...props
|
|
977
992
|
});
|
|
978
993
|
}
|
|
979
994
|
function DropdownMenuSeparator({ className, ...props }) {
|
|
980
995
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.DropdownMenu.Separator, {
|
|
981
996
|
"data-slot": "dropdown-menu-separator",
|
|
982
|
-
className: cn("-mx-1 my-1 h-px bg-border", className),
|
|
997
|
+
className: require_format.cn("-mx-1 my-1 h-px bg-border", className),
|
|
983
998
|
...props
|
|
984
999
|
});
|
|
985
1000
|
}
|
|
@@ -987,7 +1002,7 @@ function DropdownMenuSeparator({ className, ...props }) {
|
|
|
987
1002
|
/** 统一空态:图标 + 标题 + 说明 + 可选操作 */
|
|
988
1003
|
function EmptyState({ icon: Icon, title, description, action, className, compact }) {
|
|
989
1004
|
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),
|
|
1005
|
+
className: require_format.cn("flex flex-col items-center justify-center gap-2 text-center", compact ? "px-4 py-6" : "px-6 py-12", className),
|
|
991
1006
|
children: [
|
|
992
1007
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
993
1008
|
className: "flex size-10 items-center justify-center rounded-full bg-muted text-muted-foreground",
|
|
@@ -1005,41 +1020,6 @@ function EmptyState({ icon: Icon, title, description, action, className, compact
|
|
|
1005
1020
|
]
|
|
1006
1021
|
});
|
|
1007
1022
|
}
|
|
1008
|
-
// src/lib/format.ts
|
|
1009
|
-
/** 字节数 → 人类可读 */
|
|
1010
|
-
function formatBytes(bytes) {
|
|
1011
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
1012
|
-
const units = [
|
|
1013
|
-
"KB",
|
|
1014
|
-
"MB",
|
|
1015
|
-
"GB"
|
|
1016
|
-
];
|
|
1017
|
-
let value = bytes / 1024;
|
|
1018
|
-
let i = 0;
|
|
1019
|
-
while (value >= 1024 && i < units.length - 1) {
|
|
1020
|
-
value /= 1024;
|
|
1021
|
-
i += 1;
|
|
1022
|
-
}
|
|
1023
|
-
return `${value.toFixed(value >= 100 ? 0 : 1)} ${units[i]}`;
|
|
1024
|
-
}
|
|
1025
|
-
/** 像素尺寸 → `1920×1080` */
|
|
1026
|
-
function formatResolution(width, height) {
|
|
1027
|
-
if (!width || !height) return "—";
|
|
1028
|
-
return `${width}×${height}`;
|
|
1029
|
-
}
|
|
1030
|
-
/** 秒数 → `12.3s` */
|
|
1031
|
-
function formatElapsed(seconds) {
|
|
1032
|
-
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
1033
|
-
return `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`;
|
|
1034
|
-
}
|
|
1035
|
-
/** 微秒 → 秒(保留一位小数,用于表单展示) */
|
|
1036
|
-
function formatSeconds(us) {
|
|
1037
|
-
return (0, _cut_workbench_core.usToSec)(us).toFixed(2);
|
|
1038
|
-
}
|
|
1039
|
-
/** 百分比 0~1 → `42%` */
|
|
1040
|
-
function formatPercent(ratio) {
|
|
1041
|
-
return `${Math.round(ratio * 100)}%`;
|
|
1042
|
-
}
|
|
1043
1023
|
// src/features/media-library/components/DropZone.tsx
|
|
1044
1024
|
var ACCEPT = {
|
|
1045
1025
|
"video/mp4": [".mp4", ".m4v"],
|
|
@@ -1080,12 +1060,12 @@ function DropZone({ onFiles, disabled, className, compact }) {
|
|
|
1080
1060
|
});
|
|
1081
1061
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1082
1062
|
...getRootProps(),
|
|
1083
|
-
className: 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),
|
|
1063
|
+
className: require_format.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
1064
|
children: [
|
|
1085
1065
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", { ...getInputProps() }),
|
|
1086
1066
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1087
1067
|
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 ? "松开以导入" : "拖入文件,或点击选择" })]
|
|
1068
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Upload, { className: require_format.cn("text-muted-foreground", compact ? "size-3.5" : "size-4") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: compact ? "导入素材" : isDragActive ? "松开以导入" : "拖入文件,或点击选择" })]
|
|
1089
1069
|
}),
|
|
1090
1070
|
!compact && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1091
1071
|
className: "text-xs text-muted-foreground",
|
|
@@ -1107,7 +1087,7 @@ function DropZone({ onFiles, disabled, className, compact }) {
|
|
|
1107
1087
|
function Skeleton({ className, ...props }) {
|
|
1108
1088
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1109
1089
|
"data-slot": "skeleton",
|
|
1110
|
-
className: cn("animate-pulse rounded-md bg-accent", className),
|
|
1090
|
+
className: require_format.cn("animate-pulse rounded-md bg-accent", className),
|
|
1111
1091
|
...props
|
|
1112
1092
|
});
|
|
1113
1093
|
}
|
|
@@ -1156,7 +1136,7 @@ function MediaCard({ asset, selected, missing, onSelect, onAddToTimeline, onShow
|
|
|
1156
1136
|
onKeyDown: (e) => {
|
|
1157
1137
|
if (e.key === "Enter") onAddToTimeline();
|
|
1158
1138
|
},
|
|
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"),
|
|
1139
|
+
className: require_format.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
1140
|
children: [
|
|
1161
1141
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1162
1142
|
className: "relative aspect-video w-full overflow-hidden rounded-md bg-muted",
|
|
@@ -1177,7 +1157,7 @@ function MediaCard({ asset, selected, missing, onSelect, onAddToTimeline, onShow
|
|
|
1177
1157
|
opacity: .9
|
|
1178
1158
|
})
|
|
1179
1159
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1180
|
-
className: cn("flex size-full items-center justify-center", kind.tone),
|
|
1160
|
+
className: require_format.cn("flex size-full items-center justify-center", kind.tone),
|
|
1181
1161
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: "size-7" })
|
|
1182
1162
|
}),
|
|
1183
1163
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
@@ -1224,7 +1204,7 @@ function MediaCard({ asset, selected, missing, onSelect, onAddToTimeline, onShow
|
|
|
1224
1204
|
children: asset.name
|
|
1225
1205
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1226
1206
|
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 ? " · 含音轨" : ""]
|
|
1207
|
+
children: [asset.kind === "audio" ? `${require_format.formatBytes(asset.size)} · 48kHz 双声道` : asset.kind === "text" || asset.kind === "subtitle" ? `${require_format.formatResolution(asset.width, asset.height)}` : `${require_format.formatResolution(asset.width, asset.height)} · ${require_format.formatBytes(asset.size)}`, asset.hasAudio ? " · 含音轨" : ""]
|
|
1228
1208
|
})]
|
|
1229
1209
|
}),
|
|
1230
1210
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DropdownMenu, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuTrigger, {
|
|
@@ -1266,7 +1246,7 @@ function MediaGrid({ assets, selectedAssetId, missingAssetIds = [], progress, on
|
|
|
1266
1246
|
className: "grid grid-cols-2 gap-2.5 p-2.5",
|
|
1267
1247
|
children: [
|
|
1268
1248
|
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"),
|
|
1249
|
+
className: require_format.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
1250
|
children: [
|
|
1271
1251
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Skeleton, { className: "aspect-video w-full rounded-md" }),
|
|
1272
1252
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -1505,11 +1485,11 @@ function MediaLibraryPanel({ onAddToTimeline, onRemoveAsset }) {
|
|
|
1505
1485
|
}),
|
|
1506
1486
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1507
1487
|
label: "分辨率",
|
|
1508
|
-
value: formatResolution(metaAsset.width, metaAsset.height)
|
|
1488
|
+
value: require_format.formatResolution(metaAsset.width, metaAsset.height)
|
|
1509
1489
|
}),
|
|
1510
1490
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1511
1491
|
label: "大小",
|
|
1512
|
-
value: metaAsset.kind === "text" ? "—" : formatBytes(metaAsset.size)
|
|
1492
|
+
value: metaAsset.kind === "text" ? "—" : require_format.formatBytes(metaAsset.size)
|
|
1513
1493
|
}),
|
|
1514
1494
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1515
1495
|
label: "音轨",
|
|
@@ -2305,7 +2285,7 @@ function PreviewStage({ attachRef, width, height, emptyHint, onCapture, onFullsc
|
|
|
2305
2285
|
const scaleY = height > 0 ? box.h / height : 1;
|
|
2306
2286
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2307
2287
|
ref: outerRef,
|
|
2308
|
-
className: cn("relative flex min-h-0 flex-1 items-center justify-center overflow-hidden bg-stage-bg", className),
|
|
2288
|
+
className: require_format.cn("relative flex min-h-0 flex-1 items-center justify-center overflow-hidden bg-stage-bg", className),
|
|
2309
2289
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2310
2290
|
className: "relative shrink-0 overflow-hidden rounded-md bg-black shadow-2xl ring-1 ring-border",
|
|
2311
2291
|
style: {
|
|
@@ -2366,14 +2346,14 @@ function Slider({ className, defaultValue, value, min = 0, max = 100, ...props }
|
|
|
2366
2346
|
value,
|
|
2367
2347
|
min,
|
|
2368
2348
|
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),
|
|
2349
|
+
className: require_format.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
2350
|
...props,
|
|
2371
2351
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Slider.Track, {
|
|
2372
2352
|
"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"),
|
|
2353
|
+
className: require_format.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
2354
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Slider.Range, {
|
|
2375
2355
|
"data-slot": "slider-range",
|
|
2376
|
-
className: cn("absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full")
|
|
2356
|
+
className: require_format.cn("absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full")
|
|
2377
2357
|
})
|
|
2378
2358
|
}), Array.from({ length: _values.length }, (_, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Slider.Thumb, {
|
|
2379
2359
|
"data-slot": "slider-thumb",
|
|
@@ -2388,7 +2368,7 @@ function Slider({ className, defaultValue, value, min = 0, max = 100, ...props }
|
|
|
2388
2368
|
*/
|
|
2389
2369
|
function Timecode({ us, showMs = true, className, muted, title }) {
|
|
2390
2370
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2391
|
-
className: cn("font-mono text-xs tabular-nums", muted && "text-muted-foreground", className),
|
|
2371
|
+
className: require_format.cn("font-mono text-xs tabular-nums", muted && "text-muted-foreground", className),
|
|
2392
2372
|
title: title ?? `${us} µs`,
|
|
2393
2373
|
children: (0, _cut_workbench_core.formatTimecode)(us, showMs)
|
|
2394
2374
|
});
|
|
@@ -2400,15 +2380,51 @@ var RATES = [
|
|
|
2400
2380
|
1.5,
|
|
2401
2381
|
2
|
|
2402
2382
|
];
|
|
2383
|
+
/**
|
|
2384
|
+
* 紧凑档位(按容器宽度,不是视口 —— 预览列宽由拖拽决定)。
|
|
2385
|
+
*
|
|
2386
|
+
* 阈值来自实测的控件总宽:完整形态约 680px,`< 700` 先去音量滑杆与速率、
|
|
2387
|
+
* `< 560` 再去时间码的「总时长」与进度条、`< 420` 再去跳首尾。
|
|
2388
|
+
* 只靠 `shrink-0` + `overflow:hidden` 会在 650px 左右把时间码裁掉,
|
|
2389
|
+
* 且**没有任何报错**,只有截图能看出来 —— 所以这里显式分级。
|
|
2390
|
+
*/
|
|
2391
|
+
var FULL_PX = 700;
|
|
2392
|
+
var COMPACT_PX = 620;
|
|
2393
|
+
var TIGHT_PX = 520;
|
|
2394
|
+
var ULTRA_COMPACT_PX = 420;
|
|
2395
|
+
/** 低于此宽度连时间码都放不下:只留播放/逐帧,横向可滚 */
|
|
2396
|
+
var MICRO_PX = 200;
|
|
2403
2397
|
/** 播放控制条:逐帧 / 跳首尾 / 播放 / 时间码 / 速率 / 音量 */
|
|
2404
2398
|
function PlaybackControls({ playing, currentUs, durationUs, masterVolume, playbackRate, disabled, onToggle, onJumpStart, onJumpEnd, onStepFrame, onSeek, onVolumeChange, onRateChange, className }) {
|
|
2399
|
+
const rootRef = (0, react.useRef)(null);
|
|
2400
|
+
const [width, setWidth] = (0, react.useState)(0);
|
|
2401
|
+
/**
|
|
2402
|
+
* 用容器宽度而不是 CSS 媒体查询。
|
|
2403
|
+
*
|
|
2404
|
+
* 预览面板宽度由左侧拖拽决定,和视口宽度无关 —— 用户把两侧拉宽时,
|
|
2405
|
+
* 视口一点没变,`max-[560px]:hidden` 这类媒体查询不会触发,时间码照样被压扁。
|
|
2406
|
+
*/
|
|
2407
|
+
(0, react.useEffect)(() => {
|
|
2408
|
+
const el = rootRef.current;
|
|
2409
|
+
if (el == null) return;
|
|
2410
|
+
const ro = new ResizeObserver(() => setWidth(el.clientWidth));
|
|
2411
|
+
ro.observe(el);
|
|
2412
|
+
setWidth(el.clientWidth);
|
|
2413
|
+
return () => ro.disconnect();
|
|
2414
|
+
}, []);
|
|
2415
|
+
const compact = width > 0 && width < COMPACT_PX;
|
|
2416
|
+
const tight = width > 0 && width < TIGHT_PX;
|
|
2417
|
+
const ultraCompact = width > 0 && width < ULTRA_COMPACT_PX;
|
|
2418
|
+
const micro = width > 0 && width < MICRO_PX;
|
|
2419
|
+
const full = width === 0 || width >= FULL_PX;
|
|
2405
2420
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2406
|
-
|
|
2421
|
+
ref: rootRef,
|
|
2422
|
+
className: require_format.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
2423
|
children: [
|
|
2408
2424
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2409
|
-
className: "flex items-center gap-0.5",
|
|
2425
|
+
className: "flex shrink-0 items-center gap-0.5",
|
|
2410
2426
|
children: [
|
|
2411
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
2427
|
+
!ultraCompact && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
2412
2428
|
asChild: true,
|
|
2413
2429
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
2414
2430
|
variant: "ghost",
|
|
@@ -2450,7 +2466,7 @@ function PlaybackControls({ playing, currentUs, durationUs, masterVolume, playba
|
|
|
2450
2466
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.StepForward, { className: "size-3.5" })
|
|
2451
2467
|
})
|
|
2452
2468
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "下一帧 (→)" })] }),
|
|
2453
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
2469
|
+
!ultraCompact && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
2454
2470
|
asChild: true,
|
|
2455
2471
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
2456
2472
|
variant: "ghost",
|
|
@@ -2464,23 +2480,19 @@ function PlaybackControls({ playing, currentUs, durationUs, masterVolume, playba
|
|
|
2464
2480
|
]
|
|
2465
2481
|
}),
|
|
2466
2482
|
/* @__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
|
-
]
|
|
2483
|
+
className: require_format.cn("mx-2 flex shrink-0 items-baseline gap-1 whitespace-nowrap", ultraCompact && "mx-1", micro && "hidden"),
|
|
2484
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timecode, {
|
|
2485
|
+
us: currentUs,
|
|
2486
|
+
className: "text-[13px] font-medium"
|
|
2487
|
+
}), !tight && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2488
|
+
className: "text-xs text-muted-foreground",
|
|
2489
|
+
children: "/"
|
|
2490
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timecode, {
|
|
2491
|
+
us: durationUs,
|
|
2492
|
+
muted: true
|
|
2493
|
+
})] })]
|
|
2482
2494
|
}),
|
|
2483
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2495
|
+
!ultraCompact && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2484
2496
|
type: "range",
|
|
2485
2497
|
className: "mx-1 h-1 min-w-16 flex-1 cursor-pointer appearance-none rounded-full bg-muted accent-primary",
|
|
2486
2498
|
min: 0,
|
|
@@ -2492,14 +2504,14 @@ function PlaybackControls({ playing, currentUs, durationUs, masterVolume, playba
|
|
|
2492
2504
|
"aria-label": "播放进度"
|
|
2493
2505
|
}),
|
|
2494
2506
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2495
|
-
className: "flex items-center gap-1",
|
|
2507
|
+
className: require_format.cn("flex shrink-0 items-center gap-1", compact && "ml-auto", micro && "hidden"),
|
|
2496
2508
|
children: [
|
|
2497
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2509
|
+
!compact && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2498
2510
|
className: "flex items-center rounded-md border p-0.5",
|
|
2499
2511
|
children: RATES.map((rate) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2500
2512
|
type: "button",
|
|
2501
2513
|
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"),
|
|
2514
|
+
className: require_format.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
2515
|
title: rate !== 1 ? "变速预览(不会改变导出结果)" : "正常速度",
|
|
2504
2516
|
children: [rate, "×"]
|
|
2505
2517
|
}, rate))
|
|
@@ -2514,7 +2526,7 @@ function PlaybackControls({ playing, currentUs, durationUs, masterVolume, playba
|
|
|
2514
2526
|
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
2527
|
})
|
|
2516
2528
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: masterVolume > 0 ? "静音预览" : "取消静音" })] }),
|
|
2517
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Slider, {
|
|
2529
|
+
full && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Slider, {
|
|
2518
2530
|
className: "w-20",
|
|
2519
2531
|
min: 0,
|
|
2520
2532
|
max: 1,
|
|
@@ -2798,143 +2810,148 @@ var DEFAULT_BITRATE = 8e6;
|
|
|
2798
2810
|
/** 时间轴顶部工具条:分割 / 复制 / 删除 / 吸附开关 / 缩放 / 加轨道 */
|
|
2799
2811
|
function TimelineToolbar({ playheadUs, durationUs, pxPerSecond, selectionCount, magnetEnabled, onToggleMagnet, onSplit, onDuplicate, onRemove, onZoomIn, onZoomOut, onZoomToFit, onAddTrack, canZoomIn, canZoomOut }) {
|
|
2800
2812
|
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
|
-
|
|
2813
|
+
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",
|
|
2814
|
+
role: "toolbar",
|
|
2815
|
+
"aria-label": "时间轴工具",
|
|
2816
|
+
"data-timeline-toolbar": true,
|
|
2817
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2818
|
+
className: "flex shrink-0 items-center gap-1",
|
|
2819
|
+
children: [
|
|
2820
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
2821
|
+
asChild: true,
|
|
2822
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
2823
|
+
variant: "ghost",
|
|
2824
|
+
size: "icon-sm",
|
|
2825
|
+
onClick: onSplit,
|
|
2826
|
+
"aria-label": "分割",
|
|
2827
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Scissors, { className: "size-3.5" })
|
|
2828
|
+
})
|
|
2829
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "在播放头分割 (S)" })] }),
|
|
2830
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
2831
|
+
asChild: true,
|
|
2832
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
2833
|
+
variant: "ghost",
|
|
2834
|
+
size: "icon-sm",
|
|
2835
|
+
onClick: onDuplicate,
|
|
2836
|
+
disabled: selectionCount === 0,
|
|
2837
|
+
"aria-label": "复制片段",
|
|
2838
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Copy, { className: "size-3.5" })
|
|
2839
|
+
})
|
|
2840
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "复制片段 (Ctrl+D)" })] }),
|
|
2841
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
2842
|
+
asChild: true,
|
|
2843
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
2844
|
+
variant: "ghost",
|
|
2845
|
+
size: "icon-sm",
|
|
2846
|
+
onClick: onRemove,
|
|
2847
|
+
disabled: selectionCount === 0,
|
|
2848
|
+
"aria-label": "删除片段",
|
|
2849
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Trash2, { className: "size-3.5" })
|
|
2850
|
+
})
|
|
2851
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "删除片段 (Delete)" })] }),
|
|
2852
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
|
|
2853
|
+
orientation: "vertical",
|
|
2854
|
+
className: "mx-1 h-4"
|
|
2855
|
+
}),
|
|
2856
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
2857
|
+
asChild: true,
|
|
2858
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
2859
|
+
variant: magnetEnabled ? "secondary" : "ghost",
|
|
2860
|
+
size: "icon-sm",
|
|
2861
|
+
onClick: onToggleMagnet,
|
|
2862
|
+
"aria-label": "吸附",
|
|
2863
|
+
"aria-pressed": magnetEnabled,
|
|
2864
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Magnet, { className: "size-3.5" })
|
|
2865
|
+
})
|
|
2866
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "吸附对齐(阈值 15px)" })] }),
|
|
2867
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
|
|
2868
|
+
orientation: "vertical",
|
|
2869
|
+
className: "mx-1 h-4"
|
|
2870
|
+
}),
|
|
2871
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2872
|
+
className: "flex items-center gap-0.5",
|
|
2873
|
+
children: [
|
|
2874
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
2875
|
+
asChild: true,
|
|
2876
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
2877
|
+
variant: "ghost",
|
|
2878
|
+
size: "icon-sm",
|
|
2879
|
+
onClick: onZoomOut,
|
|
2880
|
+
disabled: !canZoomOut,
|
|
2881
|
+
"aria-label": "缩小",
|
|
2882
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ZoomOut, { className: "size-3.5" })
|
|
2883
|
+
})
|
|
2884
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "缩小 (-)" })] }),
|
|
2885
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2886
|
+
className: "w-11 text-center font-mono text-[10px] text-muted-foreground tabular-nums",
|
|
2887
|
+
children: Math.round(pxPerSecond)
|
|
2888
|
+
}),
|
|
2889
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
2890
|
+
asChild: true,
|
|
2891
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
2892
|
+
variant: "ghost",
|
|
2893
|
+
size: "icon-sm",
|
|
2894
|
+
onClick: onZoomIn,
|
|
2895
|
+
disabled: !canZoomIn,
|
|
2896
|
+
"aria-label": "放大",
|
|
2897
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ZoomIn, { className: "size-3.5" })
|
|
2898
|
+
})
|
|
2899
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "放大 (+)" })] }),
|
|
2900
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
2875
2901
|
variant: "ghost",
|
|
2876
|
-
size: "
|
|
2877
|
-
onClick:
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ZoomIn, { className: "size-3.5" })
|
|
2902
|
+
size: "xs",
|
|
2903
|
+
onClick: onZoomToFit,
|
|
2904
|
+
className: "text-[10px]",
|
|
2905
|
+
children: "适应"
|
|
2881
2906
|
})
|
|
2882
|
-
|
|
2883
|
-
|
|
2907
|
+
]
|
|
2908
|
+
}),
|
|
2909
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
|
|
2910
|
+
orientation: "vertical",
|
|
2911
|
+
className: "mx-1 h-4"
|
|
2912
|
+
}),
|
|
2913
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2914
|
+
className: "flex items-center gap-0.5",
|
|
2915
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
|
|
2884
2916
|
variant: "ghost",
|
|
2885
2917
|
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
|
-
]
|
|
2918
|
+
onClick: () => onAddTrack("visual"),
|
|
2919
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, { className: "size-3" }), "视频轨"]
|
|
2920
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
|
|
2921
|
+
variant: "ghost",
|
|
2922
|
+
size: "xs",
|
|
2923
|
+
onClick: () => onAddTrack("audio"),
|
|
2924
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, { className: "size-3" }), "音频轨"]
|
|
2925
|
+
})]
|
|
2926
|
+
})
|
|
2927
|
+
]
|
|
2928
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2929
|
+
className: "ml-auto hidden shrink-0 items-center gap-2 pl-2 text-[11px] text-muted-foreground @[720px]:flex",
|
|
2930
|
+
children: [
|
|
2931
|
+
selectionCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2932
|
+
className: "rounded bg-primary/15 px-1.5 py-0.5 text-primary",
|
|
2933
|
+
children: ["已选 ", selectionCount]
|
|
2934
|
+
}),
|
|
2935
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Info, { className: "size-3" }),
|
|
2936
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2937
|
+
className: "tabular-nums whitespace-nowrap",
|
|
2938
|
+
children: [
|
|
2939
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timecode, {
|
|
2940
|
+
us: playheadUs,
|
|
2941
|
+
className: "text-foreground"
|
|
2942
|
+
}),
|
|
2943
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2944
|
+
className: "mx-1",
|
|
2945
|
+
children: "/"
|
|
2946
|
+
}),
|
|
2947
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timecode, {
|
|
2948
|
+
us: durationUs,
|
|
2949
|
+
muted: true
|
|
2950
|
+
})
|
|
2951
|
+
]
|
|
2952
|
+
})
|
|
2953
|
+
]
|
|
2954
|
+
})]
|
|
2938
2955
|
});
|
|
2939
2956
|
}
|
|
2940
2957
|
// src/features/timeline/components/Ruler.tsx
|
|
@@ -2951,14 +2968,14 @@ function Ruler({ pxPerSecond, widthPx, playheadPx, scrollLeftPx, className }) {
|
|
|
2951
2968
|
widthPx
|
|
2952
2969
|
]);
|
|
2953
2970
|
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),
|
|
2971
|
+
className: require_format.cn("relative shrink-0 cursor-ew-resize border-b bg-timeline-bg select-none", className),
|
|
2955
2972
|
style: {
|
|
2956
2973
|
height: 28,
|
|
2957
2974
|
width: widthPx
|
|
2958
2975
|
},
|
|
2959
2976
|
children: [
|
|
2960
2977
|
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"),
|
|
2978
|
+
className: require_format.cn("absolute bottom-0 w-px", tick.major ? "h-2 bg-muted-foreground/50" : "h-1 bg-muted-foreground/25"),
|
|
2962
2979
|
style: { left: tick.px }
|
|
2963
2980
|
}, `${tick.us}-${tick.major ? "M" : "m"}`)),
|
|
2964
2981
|
ticks.filter((t) => t.major).map((tick) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -2986,7 +3003,7 @@ function Ruler({ pxPerSecond, widthPx, playheadPx, scrollLeftPx, className }) {
|
|
|
2986
3003
|
*/
|
|
2987
3004
|
var TrackHeader = (0, react.memo)(function TrackHeader({ track, onToggleMute, onToggleLock, onToggleHidden, onRemove }) {
|
|
2988
3005
|
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"),
|
|
3006
|
+
className: require_format.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
3007
|
style: { height: 56 },
|
|
2991
3008
|
"data-track-header": true,
|
|
2992
3009
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -3041,7 +3058,7 @@ function IconToggle({ active, onClick, label, icon: Icon }) {
|
|
|
3041
3058
|
onClick,
|
|
3042
3059
|
"aria-label": label,
|
|
3043
3060
|
"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"),
|
|
3061
|
+
className: require_format.cn("flex size-[18px] items-center justify-center rounded transition-colors", active ? "bg-primary/20 text-primary" : "text-muted-foreground hover:text-foreground"),
|
|
3045
3062
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: "size-3" })
|
|
3046
3063
|
})
|
|
3047
3064
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: label })] });
|
|
@@ -3132,7 +3149,7 @@ var ClipBlock = (0, react.memo)(function ClipBlock({ clip, asset, pxPerSecond, s
|
|
|
3132
3149
|
handlers.beginMove(event, clip);
|
|
3133
3150
|
},
|
|
3134
3151
|
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"),
|
|
3152
|
+
className: require_format.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
3153
|
style: {
|
|
3137
3154
|
left: leftPx,
|
|
3138
3155
|
width: widthPx,
|
|
@@ -3211,7 +3228,7 @@ function ContextMenuPortal({ ...props }) {
|
|
|
3211
3228
|
function ContextMenuContent({ className, ...props }) {
|
|
3212
3229
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextMenuPortal, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ContextMenu.Content, {
|
|
3213
3230
|
"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),
|
|
3231
|
+
className: require_format.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
3232
|
...props
|
|
3216
3233
|
}) });
|
|
3217
3234
|
}
|
|
@@ -3220,21 +3237,21 @@ function ContextMenuItem({ className, inset, variant = "default", ...props }) {
|
|
|
3220
3237
|
"data-slot": "context-menu-item",
|
|
3221
3238
|
"data-inset": inset,
|
|
3222
3239
|
"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),
|
|
3240
|
+
className: require_format.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
3241
|
...props
|
|
3225
3242
|
});
|
|
3226
3243
|
}
|
|
3227
3244
|
function ContextMenuSeparator({ className, ...props }) {
|
|
3228
3245
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ContextMenu.Separator, {
|
|
3229
3246
|
"data-slot": "context-menu-separator",
|
|
3230
|
-
className: cn("-mx-1 my-1 h-px bg-border", className),
|
|
3247
|
+
className: require_format.cn("-mx-1 my-1 h-px bg-border", className),
|
|
3231
3248
|
...props
|
|
3232
3249
|
});
|
|
3233
3250
|
}
|
|
3234
3251
|
function ContextMenuShortcut({ className, ...props }) {
|
|
3235
3252
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3236
3253
|
"data-slot": "context-menu-shortcut",
|
|
3237
|
-
className: cn("ml-auto text-xs tracking-widest text-muted-foreground", className),
|
|
3254
|
+
className: require_format.cn("ml-auto text-xs tracking-widest text-muted-foreground", className),
|
|
3238
3255
|
...props
|
|
3239
3256
|
});
|
|
3240
3257
|
}
|
|
@@ -3291,14 +3308,14 @@ function ClipContextMenu({ children, onSplit, onDuplicate, onRemove, onShowInfo,
|
|
|
3291
3308
|
/** 播放头:一条竖线 + 顶部把手,跨过所有轨道 */
|
|
3292
3309
|
function Playhead({ px, playing, className }) {
|
|
3293
3310
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3294
|
-
className: cn("pointer-events-none absolute top-0 bottom-0 z-20 w-px", className),
|
|
3311
|
+
className: require_format.cn("pointer-events-none absolute top-0 bottom-0 z-20 w-px", className),
|
|
3295
3312
|
style: { left: px },
|
|
3296
3313
|
"aria-hidden": true,
|
|
3297
3314
|
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)]"),
|
|
3315
|
+
className: require_format.cn("absolute inset-y-0 -left-px w-0.5 bg-playhead", playing && "shadow-[0_0_6px_var(--cw-playhead)]"),
|
|
3299
3316
|
style: { top: 24 }
|
|
3300
3317
|
}), /* @__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"),
|
|
3318
|
+
className: require_format.cn("absolute -translate-x-1/2 rounded-sm bg-playhead", playing ? "size-3 rotate-45" : "h-2.5 w-2.5 rotate-45"),
|
|
3302
3319
|
style: { top: 14 }
|
|
3303
3320
|
})]
|
|
3304
3321
|
});
|
|
@@ -3307,7 +3324,7 @@ function Playhead({ px, playing, className }) {
|
|
|
3307
3324
|
/** 框选矩形 */
|
|
3308
3325
|
function Marquee({ x1, y1, x2, y2, className }) {
|
|
3309
3326
|
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),
|
|
3327
|
+
className: require_format.cn("pointer-events-none absolute z-40 rounded-sm border border-primary/70 bg-primary/15", className),
|
|
3311
3328
|
style: {
|
|
3312
3329
|
left: Math.min(x1, x2),
|
|
3313
3330
|
top: Math.min(y1, y2),
|
|
@@ -3898,7 +3915,7 @@ function Timeline() {
|
|
|
3898
3915
|
splitClip
|
|
3899
3916
|
]);
|
|
3900
3917
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3901
|
-
className: "flex min-h-0 flex-1 flex-col",
|
|
3918
|
+
className: "flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden",
|
|
3902
3919
|
children: [
|
|
3903
3920
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimelineToolbar, {
|
|
3904
3921
|
playheadUs,
|
|
@@ -3918,7 +3935,7 @@ function Timeline() {
|
|
|
3918
3935
|
canZoomOut: zoom.canZoomOut
|
|
3919
3936
|
}),
|
|
3920
3937
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3921
|
-
className: "relative flex min-h-0 flex-1",
|
|
3938
|
+
className: "relative flex min-h-0 min-w-0 flex-1 overflow-hidden",
|
|
3922
3939
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3923
3940
|
className: "thin-scrollbar flex shrink-0 flex-col overflow-y-auto border-r",
|
|
3924
3941
|
style: { width: 150 },
|
|
@@ -3982,7 +3999,7 @@ function Timeline() {
|
|
|
3982
3999
|
"data-timeline-tracks": true,
|
|
3983
4000
|
children: [
|
|
3984
4001
|
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"),
|
|
4002
|
+
className: require_format.cn("relative border-b border-timeline-grid", track.kind === "audio" ? "bg-timeline-track/30" : "bg-timeline-track/50", track.hidden && "opacity-40"),
|
|
3986
4003
|
style: { height: 56 },
|
|
3987
4004
|
"data-track-row": track.id,
|
|
3988
4005
|
children: [
|
|
@@ -4053,7 +4070,7 @@ function Tabs({ className, orientation = "horizontal", ...props }) {
|
|
|
4053
4070
|
"data-slot": "tabs",
|
|
4054
4071
|
"data-orientation": orientation,
|
|
4055
4072
|
orientation,
|
|
4056
|
-
className: cn("group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", className),
|
|
4073
|
+
className: require_format.cn("group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", className),
|
|
4057
4074
|
...props
|
|
4058
4075
|
});
|
|
4059
4076
|
}
|
|
@@ -4068,21 +4085,21 @@ function TabsList({ className, variant = "default", ...props }) {
|
|
|
4068
4085
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Tabs.List, {
|
|
4069
4086
|
"data-slot": "tabs-list",
|
|
4070
4087
|
"data-variant": variant,
|
|
4071
|
-
className: cn(tabsListVariants({ variant }), className),
|
|
4088
|
+
className: require_format.cn(tabsListVariants({ variant }), className),
|
|
4072
4089
|
...props
|
|
4073
4090
|
});
|
|
4074
4091
|
}
|
|
4075
4092
|
function TabsTrigger({ className, ...props }) {
|
|
4076
4093
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Tabs.Trigger, {
|
|
4077
4094
|
"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),
|
|
4095
|
+
className: require_format.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
4096
|
...props
|
|
4080
4097
|
});
|
|
4081
4098
|
}
|
|
4082
4099
|
function TabsContent({ className, ...props }) {
|
|
4083
4100
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Tabs.Content, {
|
|
4084
4101
|
"data-slot": "tabs-content",
|
|
4085
|
-
className: cn("flex-1 outline-none", className),
|
|
4102
|
+
className: require_format.cn("flex-1 outline-none", className),
|
|
4086
4103
|
...props
|
|
4087
4104
|
});
|
|
4088
4105
|
}
|
|
@@ -4091,7 +4108,7 @@ function Input({ className, type, ...props }) {
|
|
|
4091
4108
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4092
4109
|
type,
|
|
4093
4110
|
"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),
|
|
4111
|
+
className: require_format.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
4112
|
...props
|
|
4096
4113
|
});
|
|
4097
4114
|
}
|
|
@@ -4112,7 +4129,7 @@ function SelectTrigger({ className, size = "default", children, ...props }) {
|
|
|
4112
4129
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Select.Trigger, {
|
|
4113
4130
|
"data-slot": "select-trigger",
|
|
4114
4131
|
"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),
|
|
4132
|
+
className: require_format.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
4133
|
...props,
|
|
4117
4134
|
children: [children, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Select.Icon, {
|
|
4118
4135
|
asChild: true,
|
|
@@ -4126,14 +4143,14 @@ function SelectContent({ className, children, position = "item-aligned", align =
|
|
|
4126
4143
|
container: container ?? void 0,
|
|
4127
4144
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Select.Content, {
|
|
4128
4145
|
"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),
|
|
4146
|
+
className: require_format.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
4147
|
position,
|
|
4131
4148
|
align,
|
|
4132
4149
|
...props,
|
|
4133
4150
|
children: [
|
|
4134
4151
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectScrollUpButton, {}),
|
|
4135
4152
|
/* @__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"),
|
|
4153
|
+
className: require_format.cn("p-1", position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"),
|
|
4137
4154
|
children
|
|
4138
4155
|
}),
|
|
4139
4156
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectScrollDownButton, {})
|
|
@@ -4144,7 +4161,7 @@ function SelectContent({ className, children, position = "item-aligned", align =
|
|
|
4144
4161
|
function SelectItem({ className, children, ...props }) {
|
|
4145
4162
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Select.Item, {
|
|
4146
4163
|
"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),
|
|
4164
|
+
className: require_format.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
4165
|
...props,
|
|
4149
4166
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4150
4167
|
"data-slot": "select-item-indicator",
|
|
@@ -4156,7 +4173,7 @@ function SelectItem({ className, children, ...props }) {
|
|
|
4156
4173
|
function SelectScrollUpButton({ className, ...props }) {
|
|
4157
4174
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Select.ScrollUpButton, {
|
|
4158
4175
|
"data-slot": "select-scroll-up-button",
|
|
4159
|
-
className: cn("flex cursor-default items-center justify-center py-1", className),
|
|
4176
|
+
className: require_format.cn("flex cursor-default items-center justify-center py-1", className),
|
|
4160
4177
|
...props,
|
|
4161
4178
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronUpIcon, { className: "size-4" })
|
|
4162
4179
|
});
|
|
@@ -4164,7 +4181,7 @@ function SelectScrollUpButton({ className, ...props }) {
|
|
|
4164
4181
|
function SelectScrollDownButton({ className, ...props }) {
|
|
4165
4182
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Select.ScrollDownButton, {
|
|
4166
4183
|
"data-slot": "select-scroll-down-button",
|
|
4167
|
-
className: cn("flex cursor-default items-center justify-center py-1", className),
|
|
4184
|
+
className: require_format.cn("flex cursor-default items-center justify-center py-1", className),
|
|
4168
4185
|
...props,
|
|
4169
4186
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDownIcon, { className: "size-4" })
|
|
4170
4187
|
});
|
|
@@ -4173,7 +4190,7 @@ function SelectScrollDownButton({ className, ...props }) {
|
|
|
4173
4190
|
/** 属性面板的一行:标签 + 控件(标签在左,控件占满剩余宽度) */
|
|
4174
4191
|
function Field({ label, hint, children, className, htmlFor }) {
|
|
4175
4192
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4176
|
-
className: cn("flex items-center gap-2", className),
|
|
4193
|
+
className: require_format.cn("flex items-center gap-2", className),
|
|
4177
4194
|
children: [
|
|
4178
4195
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Label, {
|
|
4179
4196
|
htmlFor,
|
|
@@ -4194,7 +4211,7 @@ function Field({ label, hint, children, className, htmlFor }) {
|
|
|
4194
4211
|
/** 属性分组 */
|
|
4195
4212
|
function Section({ title, children, action, className }) {
|
|
4196
4213
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
4197
|
-
className: cn("space-y-2", className),
|
|
4214
|
+
className: require_format.cn("space-y-2", className),
|
|
4198
4215
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4199
4216
|
className: "flex items-center justify-between",
|
|
4200
4217
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
@@ -4389,7 +4406,7 @@ function VideoProps({ clip, onPatch, onPatchTransform, onPatchSize }) {
|
|
|
4389
4406
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
|
|
4390
4407
|
type: "number",
|
|
4391
4408
|
className: "h-7 text-xs tabular-nums",
|
|
4392
|
-
value: formatSeconds(clip.startUs),
|
|
4409
|
+
value: require_format.formatSeconds(clip.startUs),
|
|
4393
4410
|
step: .1,
|
|
4394
4411
|
min: 0,
|
|
4395
4412
|
onChange: (e) => {
|
|
@@ -4403,7 +4420,7 @@ function VideoProps({ clip, onPatch, onPatchTransform, onPatchSize }) {
|
|
|
4403
4420
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
|
|
4404
4421
|
type: "number",
|
|
4405
4422
|
className: "h-7 text-xs tabular-nums",
|
|
4406
|
-
value: formatSeconds(clip.durationUs),
|
|
4423
|
+
value: require_format.formatSeconds(clip.durationUs),
|
|
4407
4424
|
step: .1,
|
|
4408
4425
|
min: .1,
|
|
4409
4426
|
onChange: (e) => {
|
|
@@ -4510,7 +4527,7 @@ function AudioProps({ clip, onPatch }) {
|
|
|
4510
4527
|
className: "text-[10px] text-muted-foreground",
|
|
4511
4528
|
children: [
|
|
4512
4529
|
"片段时长 ",
|
|
4513
|
-
formatSeconds(clip.durationUs),
|
|
4530
|
+
require_format.formatSeconds(clip.durationUs),
|
|
4514
4531
|
"s,淡入使用 setAnimation 线性插值。"
|
|
4515
4532
|
]
|
|
4516
4533
|
})
|
|
@@ -4765,7 +4782,7 @@ function SubtitleProps({ clip, onPatch, onRebuild }) {
|
|
|
4765
4782
|
className: "text-[10px] text-muted-foreground",
|
|
4766
4783
|
children: [
|
|
4767
4784
|
"片段时长 ",
|
|
4768
|
-
formatSeconds(clip.durationUs),
|
|
4785
|
+
require_format.formatSeconds(clip.durationUs),
|
|
4769
4786
|
"s",
|
|
4770
4787
|
cues.length > 0 && ` · 最后一条结束于 ${(cues[cues.length - 1].endMs / 1e3).toFixed(2)}s`
|
|
4771
4788
|
]
|
|
@@ -5037,7 +5054,7 @@ function InspectorPanel() {
|
|
|
5037
5054
|
title: asset.name,
|
|
5038
5055
|
children: asset.name
|
|
5039
5056
|
}),
|
|
5040
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
|
|
5057
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_format.Badge, {
|
|
5041
5058
|
variant: "outline",
|
|
5042
5059
|
className: "text-[10px]",
|
|
5043
5060
|
children: KIND_LABEL[asset.kind]
|
|
@@ -5060,7 +5077,7 @@ function InspectorPanel() {
|
|
|
5060
5077
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
5061
5078
|
(0, _cut_workbench_core.formatDuration)(clip.durationUs),
|
|
5062
5079
|
" · ",
|
|
5063
|
-
formatResolution(asset.width, asset.height)
|
|
5080
|
+
require_format.formatResolution(asset.width, asset.height)
|
|
5064
5081
|
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
5065
5082
|
"入点 ",
|
|
5066
5083
|
(clip.inPointUs / 1e6).toFixed(2),
|
|
@@ -5165,11 +5182,11 @@ function Switch({ className, size = "default", ...props }) {
|
|
|
5165
5182
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Switch.Root, {
|
|
5166
5183
|
"data-slot": "switch",
|
|
5167
5184
|
"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),
|
|
5185
|
+
className: require_format.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
5186
|
...props,
|
|
5170
5187
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Switch.Thumb, {
|
|
5171
5188
|
"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")
|
|
5189
|
+
className: require_format.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
5190
|
})
|
|
5174
5191
|
});
|
|
5175
5192
|
}
|
|
@@ -5177,7 +5194,7 @@ function Switch({ className, size = "default", ...props }) {
|
|
|
5177
5194
|
function Progress({ className, value, ...props }) {
|
|
5178
5195
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Progress.Root, {
|
|
5179
5196
|
"data-slot": "progress",
|
|
5180
|
-
className: cn("relative h-2 w-full overflow-hidden rounded-full bg-primary/20", className),
|
|
5197
|
+
className: require_format.cn("relative h-2 w-full overflow-hidden rounded-full bg-primary/20", className),
|
|
5181
5198
|
...props,
|
|
5182
5199
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Progress.Indicator, {
|
|
5183
5200
|
"data-slot": "progress-indicator",
|
|
@@ -5855,7 +5872,7 @@ function ExportDialog() {
|
|
|
5855
5872
|
className: "flex justify-between",
|
|
5856
5873
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "预估大小" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5857
5874
|
className: "font-mono tabular-nums",
|
|
5858
|
-
children: formatBytes(estimatedBytes)
|
|
5875
|
+
children: require_format.formatBytes(estimatedBytes)
|
|
5859
5876
|
})]
|
|
5860
5877
|
}),
|
|
5861
5878
|
hasTextClips && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -5892,10 +5909,10 @@ function ExportDialog() {
|
|
|
5892
5909
|
children: [
|
|
5893
5910
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.CheckCircle2, { className: "size-3.5" }),
|
|
5894
5911
|
"导出完成 · 耗时 ",
|
|
5895
|
-
formatElapsed(state.result.elapsedMs / 1e3),
|
|
5912
|
+
require_format.formatElapsed(state.result.elapsedMs / 1e3),
|
|
5896
5913
|
" ·",
|
|
5897
5914
|
" ",
|
|
5898
|
-
formatBytes(state.result.blob.size),
|
|
5915
|
+
require_format.formatBytes(state.result.blob.size),
|
|
5899
5916
|
state.result.mode === "main" && "(主线程)"
|
|
5900
5917
|
]
|
|
5901
5918
|
}),
|
|
@@ -5958,7 +5975,7 @@ function ExportProgress({ progress, label = "正在合成…", className }) {
|
|
|
5958
5975
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Loader2, { className: "size-3 animate-spin" }), label]
|
|
5959
5976
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5960
5977
|
className: "font-mono tabular-nums",
|
|
5961
|
-
children: formatPercent(progress)
|
|
5978
|
+
children: require_format.formatPercent(progress)
|
|
5962
5979
|
})]
|
|
5963
5980
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Progress, { value: progress * 100 })]
|
|
5964
5981
|
});
|
|
@@ -6034,7 +6051,7 @@ function CanvasSettingsBody({ onDone }) {
|
|
|
6034
6051
|
setDraftW(String(preset.width));
|
|
6035
6052
|
setDraftH(String(preset.height));
|
|
6036
6053
|
},
|
|
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"),
|
|
6054
|
+
className: require_format.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
6055
|
title: preset.hint,
|
|
6039
6056
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6040
6057
|
className: "block rounded-[2px] border border-current opacity-70",
|
|
@@ -6095,7 +6112,7 @@ function CanvasSettingsBody({ onDone }) {
|
|
|
6095
6112
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6096
6113
|
type: "button",
|
|
6097
6114
|
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"),
|
|
6115
|
+
className: require_format.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
6116
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6100
6117
|
className: "block font-medium text-foreground",
|
|
6101
6118
|
children: "随画布缩放"
|
|
@@ -6106,7 +6123,7 @@ function CanvasSettingsBody({ onDone }) {
|
|
|
6106
6123
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6107
6124
|
type: "button",
|
|
6108
6125
|
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"),
|
|
6126
|
+
className: require_format.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
6127
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6111
6128
|
className: "block font-medium text-foreground",
|
|
6112
6129
|
children: "保持原位置"
|
|
@@ -6414,1173 +6431,6 @@ function useCapabilityInfo() {
|
|
|
6414
6431
|
summary: (0, _cut_workbench_core_engine.summarizeCapabilities)(report)
|
|
6415
6432
|
};
|
|
6416
6433
|
}
|
|
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:屏幕 / 画布录制。
|
|
7200
|
-
*
|
|
7201
|
-
* `AVRecorder` 接收任意 MediaStream 并输出 MP4(H.264 + AAC)。
|
|
7202
|
-
* 这里提供两种源:
|
|
7203
|
-
* - 程序化画布(Canvas.captureStream,无需授权);
|
|
7204
|
-
* - 摄像头(getUserMedia,需要用户授权)。
|
|
7205
|
-
*/
|
|
7206
|
-
function RecorderLab() {
|
|
7207
|
-
const [mode, setMode] = (0, react.useState)("demo");
|
|
7208
|
-
const [recording, setRecording] = (0, react.useState)(false);
|
|
7209
|
-
const [paused, setPaused] = (0, react.useState)(false);
|
|
7210
|
-
const [bytes, setBytes] = (0, react.useState)(0);
|
|
7211
|
-
const [state, setState] = (0, react.useState)("inactive");
|
|
7212
|
-
const [result, setResult] = (0, react.useState)(null);
|
|
7213
|
-
const [previewStream, setPreviewStream] = (0, react.useState)(null);
|
|
7214
|
-
const videoRef = (0, react.useRef)(null);
|
|
7215
|
-
const recorderRef = (0, react.useRef)(null);
|
|
7216
|
-
const demoRef = (0, react.useRef)(null);
|
|
7217
|
-
const cameraRef = (0, react.useRef)(null);
|
|
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;
|
|
7245
|
-
}
|
|
7246
|
-
try {
|
|
7247
|
-
const stream = await navigator.mediaDevices.getUserMedia({
|
|
7248
|
-
video: true,
|
|
7249
|
-
audio: true
|
|
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;
|
|
7258
|
-
}
|
|
7259
|
-
};
|
|
7260
|
-
const handleStart = async () => {
|
|
7261
|
-
const stream = await acquireStream();
|
|
7262
|
-
if (stream == null) return;
|
|
7263
|
-
setBytes(0);
|
|
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);
|
|
7281
|
-
}
|
|
7282
|
-
setState(recorder.state());
|
|
7283
|
-
};
|
|
7284
|
-
const handleStop = async () => {
|
|
7285
|
-
const recorder = recorderRef.current;
|
|
7286
|
-
if (recorder == null) return;
|
|
7287
|
-
const output = await run("正在封装 MP4…", async () => {
|
|
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`);
|
|
7298
|
-
}
|
|
7299
|
-
};
|
|
7300
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LabCard, {
|
|
7301
|
-
title: "屏幕 / 画布录制",
|
|
7302
|
-
description: "用 AVRecorder 把 MediaStream 实时编码为 MP4。可录制程序化画布或摄像头。",
|
|
7303
|
-
apis: ["AVCanvas.captureStream", "AVRecorder.start/pause/resume/stop"],
|
|
7304
|
-
children: [
|
|
7305
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7306
|
-
className: "flex gap-1.5",
|
|
7307
|
-
children: ["demo", "camera"].map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7308
|
-
type: "button",
|
|
7309
|
-
disabled: recording,
|
|
7310
|
-
onClick: () => setMode(item),
|
|
7311
|
-
className: `flex-1 rounded-md border px-2 py-1 text-[11px] disabled:opacity-50 ${mode === item ? "border-primary bg-primary/10 text-primary" : "text-muted-foreground"}`,
|
|
7312
|
-
children: item === "demo" ? "动态画布" : "摄像头"
|
|
7313
|
-
}, item))
|
|
7314
|
-
}),
|
|
7315
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7316
|
-
className: "overflow-hidden rounded-md border bg-stage-bg",
|
|
7317
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("video", {
|
|
7318
|
-
ref: videoRef,
|
|
7319
|
-
muted: true,
|
|
7320
|
-
playsInline: true,
|
|
7321
|
-
className: "block w-full",
|
|
7322
|
-
style: { maxHeight: 170 }
|
|
7323
|
-
})
|
|
7324
|
-
}),
|
|
7325
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7326
|
-
className: "flex flex-wrap items-center gap-1.5",
|
|
7327
|
-
children: [!recording ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
7328
|
-
type: "button",
|
|
7329
|
-
onClick: () => void handleStart(),
|
|
7330
|
-
className: "flex items-center gap-1 rounded-md bg-destructive px-2.5 py-1 text-[11px] text-destructive-foreground",
|
|
7331
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Circle, { className: "size-3 fill-current" }), "开始录制"]
|
|
7332
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
7333
|
-
type: "button",
|
|
7334
|
-
onClick: handlePauseToggle,
|
|
7335
|
-
className: "flex items-center gap-1 rounded-md bg-secondary px-2.5 py-1 text-[11px]",
|
|
7336
|
-
children: [paused ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Play, { className: "size-3" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Pause, { className: "size-3" }), paused ? "继续" : "暂停"]
|
|
7337
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
7338
|
-
type: "button",
|
|
7339
|
-
onClick: () => void handleStop(),
|
|
7340
|
-
className: "flex items-center gap-1 rounded-md bg-primary px-2.5 py-1 text-[11px] text-primary-foreground",
|
|
7341
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Square, { className: "size-3" }), "停止"]
|
|
7342
|
-
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
7343
|
-
className: "ml-auto font-mono text-[10px] text-muted-foreground tabular-nums",
|
|
7344
|
-
children: [
|
|
7345
|
-
(bytes / 1024 / 1024).toFixed(2),
|
|
7346
|
-
" MB · ",
|
|
7347
|
-
(0, _cut_workbench_core_lab.formatRecorderState)(state)
|
|
7348
|
-
]
|
|
7349
|
-
})]
|
|
7350
|
-
}),
|
|
7351
|
-
result != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResultPreview, {
|
|
7352
|
-
blob: result,
|
|
7353
|
-
filename: "recording.mp4"
|
|
7354
|
-
})
|
|
7355
|
-
]
|
|
7356
|
-
});
|
|
7357
|
-
}
|
|
7358
|
-
/** 适配 RecorderHandle(隔离 AVRecorder 细节) */
|
|
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());
|
|
7390
|
-
}
|
|
7391
|
-
};
|
|
7392
|
-
}
|
|
7393
|
-
// src/features/lab/components/CustomClipLab.tsx
|
|
7394
|
-
/**
|
|
7395
|
-
* Lab 8:自定义 IClip。
|
|
7396
|
-
*
|
|
7397
|
-
* `FrameCounterClip` 是本项目实现的 IClip:不依赖任何媒体文件,
|
|
7398
|
-
* 逐帧用 OffscreenCanvas 画出帧号 / 进度条 / 色相变化。
|
|
7399
|
-
*
|
|
7400
|
-
* 它证明 IClip 接口(tick / ready / meta / clone / split / destroy)可以被任意扩展,
|
|
7401
|
-
* 且能直接被 Combinator 编码成 MP4。
|
|
7402
|
-
*/
|
|
7403
|
-
function CustomClipLab() {
|
|
7404
|
-
const [durationSec, setDurationSec] = (0, react.useState)(5);
|
|
7405
|
-
const [timeUs, setTimeUs] = (0, react.useState)(0);
|
|
7406
|
-
const [playing, setPlaying] = (0, react.useState)(false);
|
|
7407
|
-
const [result, setResult] = (0, react.useState)(null);
|
|
7408
|
-
const canvasRef = (0, react.useRef)(null);
|
|
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]);
|
|
7435
|
-
(0, react.useEffect)(() => {
|
|
7436
|
-
if (!playing) return;
|
|
7437
|
-
let last = performance.now();
|
|
7438
|
-
let current = timeUs;
|
|
7439
|
-
const tick = () => {
|
|
7440
|
-
const now = performance.now();
|
|
7441
|
-
current += (now - last) * 1e3;
|
|
7442
|
-
last = now;
|
|
7443
|
-
if (current >= durationUs) {
|
|
7444
|
-
setTimeUs(durationUs - 1);
|
|
7445
|
-
setPlaying(false);
|
|
7446
|
-
return;
|
|
7447
|
-
}
|
|
7448
|
-
setTimeUs(current);
|
|
7449
|
-
rafRef.current = requestAnimationFrame(tick);
|
|
7450
|
-
};
|
|
7451
|
-
rafRef.current = requestAnimationFrame(tick);
|
|
7452
|
-
return () => {
|
|
7453
|
-
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
|
|
7454
|
-
};
|
|
7455
|
-
}, [playing, durationUs]);
|
|
7456
|
-
const handleExport = async () => {
|
|
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
|
-
});
|
|
7540
|
-
}
|
|
7541
|
-
// src/features/lab/components/LabPage.tsx
|
|
7542
|
-
/**
|
|
7543
|
-
* 能力实验室:每个 WebAV 高级 API 一个可独立运行的卡片。
|
|
7544
|
-
*
|
|
7545
|
-
* 与主编辑器共享同一套能力探测结果,因此这里不再重复检测。
|
|
7546
|
-
*/
|
|
7547
|
-
function LabPage() {
|
|
7548
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7549
|
-
className: "mx-auto max-w-7xl space-y-4 p-4",
|
|
7550
|
-
children: [
|
|
7551
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
7552
|
-
className: "space-y-1",
|
|
7553
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7554
|
-
className: "flex items-center gap-2",
|
|
7555
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.FlaskConical, { className: "size-4 text-primary" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
|
|
7556
|
-
className: "text-sm font-semibold",
|
|
7557
|
-
children: "能力实验室"
|
|
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
|
-
});
|
|
7583
|
-
}
|
|
7584
6434
|
// src/CutWorkbench.tsx
|
|
7585
6435
|
/**
|
|
7586
6436
|
* 工作台容器:把「三栏骨架 + 顶栏 + 弹窗」组装成一个可配置组件。
|
|
@@ -7592,11 +6442,22 @@ function LabPage() {
|
|
|
7592
6442
|
* - 不强制能力阻断页(宿主可自行决定)。
|
|
7593
6443
|
*/
|
|
7594
6444
|
/**
|
|
6445
|
+
* Lab 的懒加载器。
|
|
6446
|
+
*
|
|
6447
|
+
* ⚠️ 用**字面量**模块说明符做动态 import,而不是先从 `./features/lab` 拿组件:
|
|
6448
|
+
* 前者会被打包成 external(`import('@cut-workbench/workbench/lab')`),由宿主
|
|
6449
|
+
* 打包器解析;后者会把 lab 整棵子树(含 `@webav/av-recorder`)拉回本包的模块图。
|
|
6450
|
+
*
|
|
6451
|
+
* 解析到 lab 还是 stub,由宿主的 `resolve.alias` 决定 —— 这是移除 lab 的
|
|
6452
|
+
* 唯一可靠方式(运行时开关删不掉已产出的 chunk,见 src/lab.ts 顶部说明)。
|
|
6453
|
+
*/
|
|
6454
|
+
var LabPage = (0, react.lazy)(() => import("@cut-workbench/workbench/lab").then((m) => ({ default: m.LabPage })));
|
|
6455
|
+
/**
|
|
7595
6456
|
* 工作台主体。
|
|
7596
6457
|
*
|
|
7597
6458
|
* 必须在 `<WorkbenchProvider>` + `<CutEditorProvider>` 之内使用。
|
|
7598
6459
|
*/
|
|
7599
|
-
function CutWorkbench({ slots, className, onSaveProject, onOpenProject, enforceCapability = true }) {
|
|
6460
|
+
function CutWorkbench({ slots, toolbar: toolbarOptions, className, onSaveProject, onOpenProject, enforceCapability = true }) {
|
|
7600
6461
|
const editor = (0, _cut_workbench_react.useCutEditor)();
|
|
7601
6462
|
const capability = useCapabilityInfo();
|
|
7602
6463
|
const leftWidth = useUi((s) => s.leftWidth);
|
|
@@ -7660,10 +6521,18 @@ function CutWorkbench({ slots, className, onSaveProject, onOpenProject, enforceC
|
|
|
7660
6521
|
onExport: handleExport,
|
|
7661
6522
|
onSaveProject: onSaveProject != null ? handleSave : void 0,
|
|
7662
6523
|
onOpenProject: onOpenProject != null ? handleOpen : void 0,
|
|
6524
|
+
brand: toolbarOptions?.brand,
|
|
6525
|
+
title: toolbarOptions?.title,
|
|
6526
|
+
status: toolbarOptions?.status,
|
|
6527
|
+
actions: toolbarOptions?.actions,
|
|
6528
|
+
showCapabilityBadge: toolbarOptions?.showCapabilityBadge,
|
|
6529
|
+
editableTitle: toolbarOptions?.editableTitle,
|
|
6530
|
+
labels: toolbarOptions?.labels,
|
|
7663
6531
|
showLab: config.features.lab
|
|
7664
6532
|
});
|
|
7665
6533
|
}, [
|
|
7666
6534
|
slots?.topBar,
|
|
6535
|
+
toolbarOptions,
|
|
7667
6536
|
capability,
|
|
7668
6537
|
handleExport,
|
|
7669
6538
|
handleSave,
|
|
@@ -7690,7 +6559,17 @@ function CutWorkbench({ slots, className, onSaveProject, onOpenProject, enforceC
|
|
|
7690
6559
|
toolbar,
|
|
7691
6560
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7692
6561
|
className: "min-h-0 flex-1 overflow-auto",
|
|
7693
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
6562
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.Suspense, {
|
|
6563
|
+
fallback: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6564
|
+
className: "flex h-full items-center justify-center",
|
|
6565
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoadingOverlay, {
|
|
6566
|
+
visible: true,
|
|
6567
|
+
title: "正在加载能力实验室…",
|
|
6568
|
+
fullscreen: false
|
|
6569
|
+
})
|
|
6570
|
+
}),
|
|
6571
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LabPage, {})
|
|
6572
|
+
})
|
|
7694
6573
|
}),
|
|
7695
6574
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExportDialog, {}),
|
|
7696
6575
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CanvasSettingsDialog, {
|
|
@@ -8768,6 +7647,7 @@ function initCutWorkbench(container, options = {}) {
|
|
|
8768
7647
|
}, (0, react.createElement)(CutWorkbench, {
|
|
8769
7648
|
className: options.className ?? "h-full w-full",
|
|
8770
7649
|
enforceCapability: options.enforceCapability ?? true,
|
|
7650
|
+
toolbar: options.toolbar,
|
|
8771
7651
|
onSaveProject: () => void instance.saveProject(),
|
|
8772
7652
|
onOpenProject: async () => {
|
|
8773
7653
|
const saved = await storage.load(namespace);
|