@cut-workbench/workbench 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/features/media-library/components/DropZone.d.ts +6 -0
- package/dist/features/media-library/lib/pick-files.d.ts +24 -0
- package/dist/hooks/use-toast-safe.d.ts +9 -1
- package/dist/index.cjs +369 -281
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +249 -161
- package/dist/index.js.map +1 -1
- package/dist/lab.cjs +31 -15
- package/dist/lab.cjs.map +1 -1
- package/dist/lab.js +23 -7
- package/dist/lab.js.map +1 -1
- package/dist/lib/notify.d.ts +40 -0
- package/dist/notify-CEwFQgjC.js +233 -0
- package/dist/notify-CEwFQgjC.js.map +1 -0
- package/dist/{format--DhqHMgX.cjs → notify-CMNwDFGw.cjs} +173 -1
- package/dist/notify-CMNwDFGw.cjs.map +1 -0
- package/dist/umd/cut-workbench.umd.js +1 -1
- package/package.json +3 -3
- package/dist/format--DhqHMgX.cjs.map +0 -1
- package/dist/format-CV2aoZZC.js +0 -109
- package/dist/format-CV2aoZZC.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const
|
|
2
|
+
const require_notify = require("./notify-CMNwDFGw.cjs");
|
|
3
3
|
let react_dom_client = require("react-dom/client");
|
|
4
4
|
let react = require("react");
|
|
5
|
-
react =
|
|
5
|
+
react = require_notify.__toESM(react, 1);
|
|
6
6
|
let _cut_workbench_core_engine = require("@cut-workbench/core/engine");
|
|
7
7
|
let _cut_workbench_core = require("@cut-workbench/core");
|
|
8
8
|
let _cut_workbench_react = require("@cut-workbench/react");
|
|
@@ -36,7 +36,7 @@ let zustand_vanilla = require("zustand/vanilla");
|
|
|
36
36
|
*/
|
|
37
37
|
function AppShell({ topBar, left, center, right, timeline, leftWidth, rightWidth, timelineHeight, leftResizer, rightResizer, timelineResizer, className }) {
|
|
38
38
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
39
|
-
className:
|
|
39
|
+
className: require_notify.cn("flex h-full min-h-0 flex-col bg-background", className),
|
|
40
40
|
children: [
|
|
41
41
|
topBar,
|
|
42
42
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -106,7 +106,7 @@ function Button({ className, variant = "default", size = "default", asChild = fa
|
|
|
106
106
|
"data-slot": "button",
|
|
107
107
|
"data-variant": variant,
|
|
108
108
|
"data-size": size,
|
|
109
|
-
className:
|
|
109
|
+
className: require_notify.cn(buttonVariants({
|
|
110
110
|
variant,
|
|
111
111
|
size,
|
|
112
112
|
className
|
|
@@ -120,7 +120,7 @@ function Separator({ className, orientation = "horizontal", decorative = true, .
|
|
|
120
120
|
"data-slot": "separator",
|
|
121
121
|
decorative,
|
|
122
122
|
orientation,
|
|
123
|
-
className:
|
|
123
|
+
className: require_notify.cn("shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", className),
|
|
124
124
|
...props
|
|
125
125
|
});
|
|
126
126
|
}
|
|
@@ -199,7 +199,7 @@ function TooltipContent({ className, sideOffset = 0, children, ...props }) {
|
|
|
199
199
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Tooltip.Content, {
|
|
200
200
|
"data-slot": "tooltip-content",
|
|
201
201
|
sideOffset,
|
|
202
|
-
className:
|
|
202
|
+
className: require_notify.cn("z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", className),
|
|
203
203
|
...props,
|
|
204
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" })]
|
|
205
205
|
})
|
|
@@ -226,7 +226,7 @@ function PopoverContent({ className, align = "center", sideOffset = 4, ...props
|
|
|
226
226
|
"data-slot": "popover-content",
|
|
227
227
|
align,
|
|
228
228
|
sideOffset,
|
|
229
|
-
className:
|
|
229
|
+
className: require_notify.cn("z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", className),
|
|
230
230
|
...props
|
|
231
231
|
})
|
|
232
232
|
});
|
|
@@ -237,7 +237,7 @@ function CapabilityBadge({ info }) {
|
|
|
237
237
|
const { status, report, summary } = info;
|
|
238
238
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Popover, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PopoverTrigger, {
|
|
239
239
|
asChild: true,
|
|
240
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(
|
|
240
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_notify.Badge, {
|
|
241
241
|
variant: "outline",
|
|
242
242
|
className: "cursor-pointer gap-1 text-[11px] font-normal select-none",
|
|
243
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]
|
|
@@ -306,44 +306,6 @@ function Row$1({ label, ok, optional }) {
|
|
|
306
306
|
})]
|
|
307
307
|
});
|
|
308
308
|
}
|
|
309
|
-
// src/context.tsx
|
|
310
|
-
/**
|
|
311
|
-
* 工作台自身的 Context:UI 状态(面板尺寸 / 缩放 / 对话框)+ 宿主桥。
|
|
312
|
-
*
|
|
313
|
-
* 与 `@cut-workbench/react` 的编辑器 Context 分开:编辑器是「数据与引擎」,
|
|
314
|
-
* 这里是「某一次挂载的布局与交互」。两者生命周期可以不同(例如宿主切换项目时
|
|
315
|
-
* 复用同一个编辑器、只重建 UI)。
|
|
316
|
-
*/
|
|
317
|
-
var WorkbenchContext = (0, react.createContext)(null);
|
|
318
|
-
function useWorkbench() {
|
|
319
|
-
const value = (0, react.useContext)(WorkbenchContext);
|
|
320
|
-
if (value == null) throw new Error("[cut-workbench] 缺少 <WorkbenchProvider>:组件必须挂在工作台内部");
|
|
321
|
-
return value;
|
|
322
|
-
}
|
|
323
|
-
/** 订阅 UI 状态(带 selector) */
|
|
324
|
-
function useUi(selector) {
|
|
325
|
-
const { ui } = useWorkbench();
|
|
326
|
-
return (0, react.useSyncExternalStore)(ui.subscribe, () => selector(ui.getState()), () => selector(ui.getState()));
|
|
327
|
-
}
|
|
328
|
-
/** 取 UI store 本体(事件回调里读写用) */
|
|
329
|
-
function useUiStoreApi() {
|
|
330
|
-
return useWorkbench().ui;
|
|
331
|
-
}
|
|
332
|
-
/** 默认功能开关 */
|
|
333
|
-
var DEFAULT_FEATURES = {
|
|
334
|
-
mediaLibrary: true,
|
|
335
|
-
preview: true,
|
|
336
|
-
timeline: true,
|
|
337
|
-
inspector: true,
|
|
338
|
-
export: true,
|
|
339
|
-
canvasSettings: true,
|
|
340
|
-
lab: false
|
|
341
|
-
};
|
|
342
|
-
var DEFAULT_CONFIG = {
|
|
343
|
-
features: DEFAULT_FEATURES,
|
|
344
|
-
keyboard: "global",
|
|
345
|
-
resizable: true
|
|
346
|
-
};
|
|
347
309
|
// src/components/layout/WorkbenchToolbar.tsx
|
|
348
310
|
/**
|
|
349
311
|
* 内置顶栏(WorkbenchToolbar)。
|
|
@@ -368,9 +330,9 @@ function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject,
|
|
|
368
330
|
const redo = (0, _cut_workbench_react.useEditor)((s) => s.redo);
|
|
369
331
|
const canUndo = (0, _cut_workbench_react.useHistory)((s) => s.past.length > 0);
|
|
370
332
|
const canRedo = (0, _cut_workbench_react.useHistory)((s) => s.future.length > 0);
|
|
371
|
-
const route = useUi((s) => s.route);
|
|
372
|
-
const setRoute = useUi((s) => s.setRoute);
|
|
373
|
-
const openDialog = useUi((s) => s.openDialog);
|
|
333
|
+
const route = require_notify.useUi((s) => s.route);
|
|
334
|
+
const setRoute = require_notify.useUi((s) => s.setRoute);
|
|
335
|
+
const openDialog = require_notify.useUi((s) => s.openDialog);
|
|
374
336
|
const clipCount = (0, _cut_workbench_react.useEditor)((s) => s.project.clips.length);
|
|
375
337
|
const projectWidth = (0, _cut_workbench_react.useEditor)((s) => s.project.width);
|
|
376
338
|
const projectHeight = (0, _cut_workbench_react.useEditor)((s) => s.project.height);
|
|
@@ -530,7 +492,7 @@ function ProjectTitle({ name, dirty, placeholder, editable, onRename }) {
|
|
|
530
492
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
531
493
|
className: "flex min-w-0 items-center gap-1.5",
|
|
532
494
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
533
|
-
className:
|
|
495
|
+
className: require_notify.cn("truncate text-sm font-medium", editable && "cursor-text"),
|
|
534
496
|
title: editable ? "双击重命名" : name.length > 0 ? name : placeholder,
|
|
535
497
|
onDoubleClick: editable ? () => {
|
|
536
498
|
setDraft(name.length > 0 ? name : "");
|
|
@@ -621,8 +583,8 @@ function PanelResizer({ orientation, value, min, max, invert, onChange, classNam
|
|
|
621
583
|
"aria-orientation": isVertical ? "vertical" : "horizontal",
|
|
622
584
|
"aria-label": label,
|
|
623
585
|
onPointerDown,
|
|
624
|
-
className:
|
|
625
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className:
|
|
586
|
+
className: require_notify.cn("group relative z-20 shrink-0 touch-none transition-colors", isVertical ? "w-1 cursor-col-resize" : "h-1 cursor-row-resize", dragging ? "bg-primary/60" : "bg-border hover:bg-primary/40", className),
|
|
587
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: require_notify.cn("absolute", isVertical ? "-left-1 -right-1 inset-y-0" : "-top-1 -bottom-1 inset-x-0") })
|
|
626
588
|
});
|
|
627
589
|
}
|
|
628
590
|
// src/components/common/LoadingOverlay.tsx
|
|
@@ -630,7 +592,7 @@ function PanelResizer({ orientation, value, min, max, invert, onChange, classNam
|
|
|
630
592
|
function LoadingOverlay({ visible, title = "处理中…", description, progress, className, fullscreen, children }) {
|
|
631
593
|
if (!visible) return null;
|
|
632
594
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
633
|
-
className:
|
|
595
|
+
className: require_notify.cn("z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm", fullscreen ? "fixed inset-0" : "absolute inset-0", className),
|
|
634
596
|
role: "status",
|
|
635
597
|
"aria-live": "polite",
|
|
636
598
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -711,18 +673,18 @@ function UnsupportedBrowser({ report, loading, error }) {
|
|
|
711
673
|
const dependenciesMissing = missing.length > 0;
|
|
712
674
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
713
675
|
className: "flex min-h-screen items-center justify-center bg-background p-6",
|
|
714
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(
|
|
676
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_notify.Card, {
|
|
715
677
|
className: "w-full max-w-2xl",
|
|
716
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(
|
|
678
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_notify.CardHeader, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
717
679
|
className: "flex items-center gap-2 text-destructive",
|
|
718
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.AlertOctagon, { className: "size-5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
680
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.AlertOctagon, { className: "size-5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_notify.CardTitle, {
|
|
719
681
|
className: "text-base",
|
|
720
682
|
children: dependenciesMissing ? "缺少引擎依赖" : "当前浏览器无法运行本应用"
|
|
721
683
|
})]
|
|
722
684
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
723
685
|
className: "text-sm text-muted-foreground",
|
|
724
686
|
children: dependenciesMissing ? "工作台的预览与导出依赖 WebAV(@webav/av-cliper、@webav/av-canvas)。当前项目里没有解析到它们。" : "WebAV 依赖 WebCodecs 与 OPFS,仅支持 Chromium 内核浏览器。请使用最新版 Chrome 或 Edge(非无痕模式)重新打开。"
|
|
725
|
-
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(
|
|
687
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_notify.CardContent, {
|
|
726
688
|
className: "space-y-4",
|
|
727
689
|
children: [
|
|
728
690
|
dependenciesMissing && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -816,7 +778,7 @@ var Toaster = (props) => {
|
|
|
816
778
|
function Textarea({ className, ...props }) {
|
|
817
779
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
818
780
|
"data-slot": "textarea",
|
|
819
|
-
className:
|
|
781
|
+
className: require_notify.cn("flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40", className),
|
|
820
782
|
...props
|
|
821
783
|
});
|
|
822
784
|
}
|
|
@@ -824,7 +786,7 @@ function Textarea({ className, ...props }) {
|
|
|
824
786
|
function Label({ className, ...props }) {
|
|
825
787
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Label.Root, {
|
|
826
788
|
"data-slot": "label",
|
|
827
|
-
className:
|
|
789
|
+
className: require_notify.cn("flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", className),
|
|
828
790
|
...props
|
|
829
791
|
});
|
|
830
792
|
}
|
|
@@ -832,7 +794,7 @@ function Label({ className, ...props }) {
|
|
|
832
794
|
function ScrollArea({ className, children, ...props }) {
|
|
833
795
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.ScrollArea.Root, {
|
|
834
796
|
"data-slot": "scroll-area",
|
|
835
|
-
className:
|
|
797
|
+
className: require_notify.cn("relative", className),
|
|
836
798
|
...props,
|
|
837
799
|
children: [
|
|
838
800
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ScrollArea.Viewport, {
|
|
@@ -849,7 +811,7 @@ function ScrollBar({ className, orientation = "vertical", ...props }) {
|
|
|
849
811
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ScrollArea.ScrollAreaScrollbar, {
|
|
850
812
|
"data-slot": "scroll-area-scrollbar",
|
|
851
813
|
orientation,
|
|
852
|
-
className:
|
|
814
|
+
className: require_notify.cn("flex touch-none p-px transition-colors select-none", orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent", orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent", className),
|
|
853
815
|
...props,
|
|
854
816
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ScrollArea.ScrollAreaThumb, {
|
|
855
817
|
"data-slot": "scroll-area-thumb",
|
|
@@ -888,7 +850,7 @@ function DialogPortal({ ...props }) {
|
|
|
888
850
|
function DialogOverlay({ className, ...props }) {
|
|
889
851
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Dialog.Overlay, {
|
|
890
852
|
"data-slot": "dialog-overlay",
|
|
891
|
-
className:
|
|
853
|
+
className: require_notify.cn("fixed inset-0 z-50 bg-overlay data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0", className),
|
|
892
854
|
...props
|
|
893
855
|
});
|
|
894
856
|
}
|
|
@@ -897,7 +859,7 @@ function DialogContent({ className, children, showCloseButton = true, ...props }
|
|
|
897
859
|
"data-slot": "dialog-portal",
|
|
898
860
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogOverlay, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Dialog.Content, {
|
|
899
861
|
"data-slot": "dialog-content",
|
|
900
|
-
className:
|
|
862
|
+
className: require_notify.cn("fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg", className),
|
|
901
863
|
...props,
|
|
902
864
|
children: [children, showCloseButton && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Dialog.Close, {
|
|
903
865
|
"data-slot": "dialog-close",
|
|
@@ -913,14 +875,14 @@ function DialogContent({ className, children, showCloseButton = true, ...props }
|
|
|
913
875
|
function DialogHeader({ className, ...props }) {
|
|
914
876
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
915
877
|
"data-slot": "dialog-header",
|
|
916
|
-
className:
|
|
878
|
+
className: require_notify.cn("flex flex-col gap-2 text-center sm:text-left", className),
|
|
917
879
|
...props
|
|
918
880
|
});
|
|
919
881
|
}
|
|
920
882
|
function DialogFooter({ className, showCloseButton = false, children, ...props }) {
|
|
921
883
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
922
884
|
"data-slot": "dialog-footer",
|
|
923
|
-
className:
|
|
885
|
+
className: require_notify.cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className),
|
|
924
886
|
...props,
|
|
925
887
|
children: [children, showCloseButton && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Dialog.Close, {
|
|
926
888
|
asChild: true,
|
|
@@ -934,14 +896,14 @@ function DialogFooter({ className, showCloseButton = false, children, ...props }
|
|
|
934
896
|
function DialogTitle({ className, ...props }) {
|
|
935
897
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Dialog.Title, {
|
|
936
898
|
"data-slot": "dialog-title",
|
|
937
|
-
className:
|
|
899
|
+
className: require_notify.cn("text-lg leading-none font-semibold", className),
|
|
938
900
|
...props
|
|
939
901
|
});
|
|
940
902
|
}
|
|
941
903
|
function DialogDescription({ className, ...props }) {
|
|
942
904
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Dialog.Description, {
|
|
943
905
|
"data-slot": "dialog-description",
|
|
944
|
-
className:
|
|
906
|
+
className: require_notify.cn("text-sm text-muted-foreground", className),
|
|
945
907
|
...props
|
|
946
908
|
});
|
|
947
909
|
}
|
|
@@ -970,7 +932,7 @@ function DropdownMenuContent({ className, sideOffset = 4, ...props }) {
|
|
|
970
932
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuPortal, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.DropdownMenu.Content, {
|
|
971
933
|
"data-slot": "dropdown-menu-content",
|
|
972
934
|
sideOffset,
|
|
973
|
-
className:
|
|
935
|
+
className: require_notify.cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", className),
|
|
974
936
|
...props
|
|
975
937
|
}) });
|
|
976
938
|
}
|
|
@@ -979,7 +941,7 @@ function DropdownMenuItem({ className, inset, variant = "default", ...props }) {
|
|
|
979
941
|
"data-slot": "dropdown-menu-item",
|
|
980
942
|
"data-inset": inset,
|
|
981
943
|
"data-variant": variant,
|
|
982
|
-
className:
|
|
944
|
+
className: require_notify.cn("relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!", className),
|
|
983
945
|
...props
|
|
984
946
|
});
|
|
985
947
|
}
|
|
@@ -987,14 +949,14 @@ function DropdownMenuLabel({ className, inset, ...props }) {
|
|
|
987
949
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.DropdownMenu.Label, {
|
|
988
950
|
"data-slot": "dropdown-menu-label",
|
|
989
951
|
"data-inset": inset,
|
|
990
|
-
className:
|
|
952
|
+
className: require_notify.cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className),
|
|
991
953
|
...props
|
|
992
954
|
});
|
|
993
955
|
}
|
|
994
956
|
function DropdownMenuSeparator({ className, ...props }) {
|
|
995
957
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.DropdownMenu.Separator, {
|
|
996
958
|
"data-slot": "dropdown-menu-separator",
|
|
997
|
-
className:
|
|
959
|
+
className: require_notify.cn("-mx-1 my-1 h-px bg-border", className),
|
|
998
960
|
...props
|
|
999
961
|
});
|
|
1000
962
|
}
|
|
@@ -1002,7 +964,7 @@ function DropdownMenuSeparator({ className, ...props }) {
|
|
|
1002
964
|
/** 统一空态:图标 + 标题 + 说明 + 可选操作 */
|
|
1003
965
|
function EmptyState({ icon: Icon, title, description, action, className, compact }) {
|
|
1004
966
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1005
|
-
className:
|
|
967
|
+
className: require_notify.cn("flex flex-col items-center justify-center gap-2 text-center", compact ? "px-4 py-6" : "px-6 py-12", className),
|
|
1006
968
|
children: [
|
|
1007
969
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1008
970
|
className: "flex size-10 items-center justify-center rounded-full bg-muted text-muted-foreground",
|
|
@@ -1020,8 +982,164 @@ function EmptyState({ icon: Icon, title, description, action, className, compact
|
|
|
1020
982
|
]
|
|
1021
983
|
});
|
|
1022
984
|
}
|
|
985
|
+
// src/features/media-library/lib/pick-files.ts
|
|
986
|
+
var INPUT_ID = "cut-workbench-file-input";
|
|
987
|
+
/** 内置回退:隐藏 input,选完即弃 */
|
|
988
|
+
function pickWithInput(accept, multiple) {
|
|
989
|
+
return new Promise((resolve) => {
|
|
990
|
+
const input = document.createElement("input");
|
|
991
|
+
input.type = "file";
|
|
992
|
+
input.id = INPUT_ID;
|
|
993
|
+
input.multiple = multiple;
|
|
994
|
+
if (accept.length > 0) input.accept = accept.join(",");
|
|
995
|
+
input.style.display = "none";
|
|
996
|
+
document.body.appendChild(input);
|
|
997
|
+
const finish = (files) => {
|
|
998
|
+
input.remove();
|
|
999
|
+
resolve(files);
|
|
1000
|
+
};
|
|
1001
|
+
input.addEventListener("change", () => finish(Array.from(input.files ?? [])), { once: true });
|
|
1002
|
+
window.addEventListener("focus", () => {
|
|
1003
|
+
window.setTimeout(() => {
|
|
1004
|
+
if (document.body.contains(input)) finish([]);
|
|
1005
|
+
}, 300);
|
|
1006
|
+
}, { once: true });
|
|
1007
|
+
input.click();
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* @returns 选中的文件;用户取消时为空数组
|
|
1012
|
+
*/
|
|
1013
|
+
async function pickMediaFiles(bridge, options) {
|
|
1014
|
+
const multiple = options.multiple ?? true;
|
|
1015
|
+
if (bridge?.files?.pickFiles != null) {
|
|
1016
|
+
const result = await bridge.files.pickFiles({
|
|
1017
|
+
accept: options.accept,
|
|
1018
|
+
multiple,
|
|
1019
|
+
description: options.description
|
|
1020
|
+
});
|
|
1021
|
+
if (result != null) return result;
|
|
1022
|
+
}
|
|
1023
|
+
return pickWithInput(options.accept, multiple);
|
|
1024
|
+
}
|
|
1025
|
+
// src/features/media-library/hooks/useMediaImport.ts
|
|
1026
|
+
var ACCEPT = [
|
|
1027
|
+
"video/mp4",
|
|
1028
|
+
"audio/*",
|
|
1029
|
+
"image/*",
|
|
1030
|
+
".srt",
|
|
1031
|
+
"text/plain"
|
|
1032
|
+
];
|
|
1033
|
+
/**
|
|
1034
|
+
* 找出可以「重连」的素材。
|
|
1035
|
+
*
|
|
1036
|
+
* 场景:刷新后工程从 IndexedDB 恢复,但素材二进制消失(File 无法持久化)。
|
|
1037
|
+
* 用户重新导入同一个文件时,复用原 assetId,时间轴上已有片段立即恢复。
|
|
1038
|
+
* 匹配条件:同名 + 同类型 + 当前确实缺少二进制。
|
|
1039
|
+
*/
|
|
1040
|
+
function findReconnectableAssetId(state, file, registry) {
|
|
1041
|
+
if (state.missingAssetIds.length === 0) return null;
|
|
1042
|
+
return state.assets.find((asset) => state.missingAssetIds.includes(asset.id) && asset.name === file.name && registry.getFile(asset.id) == null)?.id ?? null;
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* 批量导入素材。
|
|
1046
|
+
*
|
|
1047
|
+
* - 串行处理,避免同时开辟多个解码器把内存打爆;
|
|
1048
|
+
* - 单个文件失败只影响自身,最后汇总 toast;
|
|
1049
|
+
* - 进度写在本地 state(不放进 store,避免高频更新引起全局重渲染);
|
|
1050
|
+
* - 若与「缺失源文件」的素材同名同类型,则重连而不是新增。
|
|
1051
|
+
*/
|
|
1052
|
+
function useMediaImport() {
|
|
1053
|
+
const editor = (0, _cut_workbench_react.useCutEditor)();
|
|
1054
|
+
const clips = editor.runtime.clips;
|
|
1055
|
+
const store = editor.store;
|
|
1056
|
+
const [progress, setProgress] = (0, react.useState)([]);
|
|
1057
|
+
const [importing, setImporting] = (0, react.useState)(false);
|
|
1058
|
+
const addAsset = (0, _cut_workbench_react.useEditor)((s) => s.addAsset);
|
|
1059
|
+
const replaceAsset = (0, _cut_workbench_react.useEditor)((s) => s.replaceAsset);
|
|
1060
|
+
const clearMissing = (0, _cut_workbench_react.useEditor)((s) => s.clearMissing);
|
|
1061
|
+
const selectAsset = (0, _cut_workbench_react.useEditor)((s) => s.selectAsset);
|
|
1062
|
+
const beginImport = require_notify.useUi((s) => s.beginImport);
|
|
1063
|
+
const endImport = require_notify.useUi((s) => s.endImport);
|
|
1064
|
+
const { bridge } = require_notify.useWorkbench();
|
|
1065
|
+
return {
|
|
1066
|
+
importing,
|
|
1067
|
+
progress,
|
|
1068
|
+
importFiles: (0, react.useCallback)(async (files) => {
|
|
1069
|
+
if (files.length === 0) return [];
|
|
1070
|
+
setImporting(true);
|
|
1071
|
+
beginImport();
|
|
1072
|
+
setProgress(files.map((f) => ({
|
|
1073
|
+
name: f.name,
|
|
1074
|
+
progress: 0
|
|
1075
|
+
})));
|
|
1076
|
+
const imported = [];
|
|
1077
|
+
const failures = [];
|
|
1078
|
+
let reconnected = 0;
|
|
1079
|
+
for (let i = 0; i < files.length; i += 1) {
|
|
1080
|
+
const file = files[i];
|
|
1081
|
+
if (!file) continue;
|
|
1082
|
+
try {
|
|
1083
|
+
const reconnectId = findReconnectableAssetId(store.getState(), file, clips);
|
|
1084
|
+
const result = await (0, _cut_workbench_core_engine.importFile)(clips, file, (p) => {
|
|
1085
|
+
setProgress((prev) => prev.map((item, index) => index === i ? {
|
|
1086
|
+
...item,
|
|
1087
|
+
progress: p
|
|
1088
|
+
} : item));
|
|
1089
|
+
});
|
|
1090
|
+
if (reconnectId != null) {
|
|
1091
|
+
clips.replaceAssetSource(reconnectId, file, result.master ?? null);
|
|
1092
|
+
replaceAsset(reconnectId, result.asset);
|
|
1093
|
+
clearMissing(reconnectId);
|
|
1094
|
+
imported.push({
|
|
1095
|
+
...result.asset,
|
|
1096
|
+
id: reconnectId
|
|
1097
|
+
});
|
|
1098
|
+
reconnected += 1;
|
|
1099
|
+
} else {
|
|
1100
|
+
addAsset(result.asset);
|
|
1101
|
+
imported.push(result.asset);
|
|
1102
|
+
}
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
const message = err instanceof _cut_workbench_core_engine.UnsupportedFileError ? err.message : err instanceof Error ? err.message : String(err);
|
|
1105
|
+
failures.push(`${file.name}:${message}`);
|
|
1106
|
+
setProgress((prev) => prev.map((item, index) => index === i ? {
|
|
1107
|
+
...item,
|
|
1108
|
+
error: message
|
|
1109
|
+
} : item));
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
setImporting(false);
|
|
1113
|
+
endImport();
|
|
1114
|
+
if (imported.length > 0) {
|
|
1115
|
+
selectAsset(imported[0]?.id ?? null);
|
|
1116
|
+
require_notify.notify(bridge, {
|
|
1117
|
+
level: "success",
|
|
1118
|
+
title: reconnected > 0 ? `已导入 ${imported.length} 个素材(其中 ${reconnected} 个重连到已有片段)` : `已导入 ${imported.length} 个素材`
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
if (failures.length > 0) require_notify.notify(bridge, {
|
|
1122
|
+
level: "error",
|
|
1123
|
+
title: `${failures.length} 个文件导入失败`,
|
|
1124
|
+
description: failures.slice(0, 3).join("\n"),
|
|
1125
|
+
duration: 8e3
|
|
1126
|
+
});
|
|
1127
|
+
window.setTimeout(() => setProgress([]), 3e3);
|
|
1128
|
+
return imported;
|
|
1129
|
+
}, [
|
|
1130
|
+
addAsset,
|
|
1131
|
+
beginImport,
|
|
1132
|
+
bridge,
|
|
1133
|
+
clearMissing,
|
|
1134
|
+
endImport,
|
|
1135
|
+
replaceAsset,
|
|
1136
|
+
selectAsset
|
|
1137
|
+
])
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1023
1140
|
// src/features/media-library/components/DropZone.tsx
|
|
1024
|
-
|
|
1141
|
+
/** react-dropzone 用的 MIME → 扩展名映射 */
|
|
1142
|
+
var DROPZONE_ACCEPT = {
|
|
1025
1143
|
"video/mp4": [".mp4", ".m4v"],
|
|
1026
1144
|
"audio/*": [
|
|
1027
1145
|
".mp3",
|
|
@@ -1044,28 +1162,51 @@ var ACCEPT = {
|
|
|
1044
1162
|
/**
|
|
1045
1163
|
* 拖拽 / 点击导入区域。
|
|
1046
1164
|
*
|
|
1165
|
+
* 两条入口:
|
|
1166
|
+
* - **拖拽**:走 react-dropzone(宿主无法介入,这是浏览器原生行为);
|
|
1167
|
+
* - **点击**:优先走 `HostBridge.files.pickFiles` —— 宿主可能不用系统文件
|
|
1168
|
+
* 对话框,而是从自己的素材库 / 云端 / URL 导入。宿主返回 `null`(不接管)
|
|
1169
|
+
* 时才回退到内置 `<input type=file>`。
|
|
1170
|
+
*
|
|
1047
1171
|
* 注意:react-dropzone 的 accept 只做初筛,真正按 MIME + 扩展名校验在
|
|
1048
1172
|
* core/media/file-reader,从而保证「拖入 .webm 得到明确错误」而不是被静默拒绝。
|
|
1049
1173
|
*/
|
|
1050
1174
|
function DropZone({ onFiles, disabled, className, compact }) {
|
|
1051
|
-
const {
|
|
1175
|
+
const { bridge } = require_notify.useWorkbench();
|
|
1176
|
+
const { getRootProps, getInputProps, isDragActive } = (0, react_dropzone.useDropzone)({
|
|
1052
1177
|
onDrop: (accepted) => {
|
|
1053
1178
|
if (accepted.length > 0) onFiles(accepted);
|
|
1054
1179
|
},
|
|
1055
|
-
noClick:
|
|
1056
|
-
noKeyboard:
|
|
1180
|
+
noClick: true,
|
|
1181
|
+
noKeyboard: true,
|
|
1057
1182
|
multiple: true,
|
|
1058
1183
|
disabled,
|
|
1059
|
-
accept:
|
|
1060
|
-
});
|
|
1184
|
+
accept: DROPZONE_ACCEPT
|
|
1185
|
+
});
|
|
1186
|
+
const handleClick = (0, react.useCallback)(() => {
|
|
1187
|
+
if (disabled) return;
|
|
1188
|
+
pickMediaFiles(bridge, {
|
|
1189
|
+
accept: ACCEPT,
|
|
1190
|
+
multiple: true,
|
|
1191
|
+
description: "选择要导入的素材"
|
|
1192
|
+
}).then((files) => {
|
|
1193
|
+
if (files.length > 0) onFiles(files);
|
|
1194
|
+
});
|
|
1195
|
+
}, [
|
|
1196
|
+
bridge,
|
|
1197
|
+
disabled,
|
|
1198
|
+
onFiles
|
|
1199
|
+
]);
|
|
1061
1200
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1062
1201
|
...getRootProps(),
|
|
1063
|
-
|
|
1202
|
+
onClick: handleClick,
|
|
1203
|
+
"data-drop-zone": "",
|
|
1204
|
+
className: require_notify.cn("group relative flex cursor-pointer flex-col items-center justify-center gap-1.5 rounded-lg border border-dashed text-center transition-colors", compact ? "px-3 py-2.5" : "px-4 py-8", isDragActive ? "border-primary bg-primary/10" : "border-border hover:border-primary/60 hover:bg-muted/40", disabled && "pointer-events-none opacity-50", className),
|
|
1064
1205
|
children: [
|
|
1065
1206
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", { ...getInputProps() }),
|
|
1066
1207
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1067
1208
|
className: "flex items-center gap-2 text-sm font-medium",
|
|
1068
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Upload, { className:
|
|
1209
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Upload, { className: require_notify.cn("text-muted-foreground", compact ? "size-3.5" : "size-4") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: compact ? "导入素材" : isDragActive ? "松开以导入" : "拖入文件,或点击选择" })]
|
|
1069
1210
|
}),
|
|
1070
1211
|
!compact && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1071
1212
|
className: "text-xs text-muted-foreground",
|
|
@@ -1076,7 +1217,7 @@ function DropZone({ onFiles, disabled, className, compact }) {
|
|
|
1076
1217
|
className: "sr-only",
|
|
1077
1218
|
onClick: (e) => {
|
|
1078
1219
|
e.stopPropagation();
|
|
1079
|
-
|
|
1220
|
+
handleClick();
|
|
1080
1221
|
},
|
|
1081
1222
|
children: "选择文件"
|
|
1082
1223
|
})
|
|
@@ -1087,7 +1228,7 @@ function DropZone({ onFiles, disabled, className, compact }) {
|
|
|
1087
1228
|
function Skeleton({ className, ...props }) {
|
|
1088
1229
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1089
1230
|
"data-slot": "skeleton",
|
|
1090
|
-
className:
|
|
1231
|
+
className: require_notify.cn("animate-pulse rounded-md bg-accent", className),
|
|
1091
1232
|
...props
|
|
1092
1233
|
});
|
|
1093
1234
|
}
|
|
@@ -1136,7 +1277,7 @@ function MediaCard({ asset, selected, missing, onSelect, onAddToTimeline, onShow
|
|
|
1136
1277
|
onKeyDown: (e) => {
|
|
1137
1278
|
if (e.key === "Enter") onAddToTimeline();
|
|
1138
1279
|
},
|
|
1139
|
-
className:
|
|
1280
|
+
className: require_notify.cn("group relative flex cursor-pointer flex-col gap-1.5 rounded-lg border p-1.5 text-left transition-colors outline-none", missing ? "border-warning/50 bg-warning/5" : selected ? "border-primary bg-primary/10 ring-1 ring-primary/40" : "border-border bg-card hover:border-primary/40 hover:bg-muted/40 focus-visible:border-primary"),
|
|
1140
1281
|
children: [
|
|
1141
1282
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1142
1283
|
className: "relative aspect-video w-full overflow-hidden rounded-md bg-muted",
|
|
@@ -1157,7 +1298,7 @@ function MediaCard({ asset, selected, missing, onSelect, onAddToTimeline, onShow
|
|
|
1157
1298
|
opacity: .9
|
|
1158
1299
|
})
|
|
1159
1300
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1160
|
-
className:
|
|
1301
|
+
className: require_notify.cn("flex size-full items-center justify-center", kind.tone),
|
|
1161
1302
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: "size-7" })
|
|
1162
1303
|
}),
|
|
1163
1304
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
@@ -1204,7 +1345,7 @@ function MediaCard({ asset, selected, missing, onSelect, onAddToTimeline, onShow
|
|
|
1204
1345
|
children: asset.name
|
|
1205
1346
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1206
1347
|
className: "truncate text-[10px] text-muted-foreground",
|
|
1207
|
-
children: [asset.kind === "audio" ? `${
|
|
1348
|
+
children: [asset.kind === "audio" ? `${require_notify.formatBytes(asset.size)} · 48kHz 双声道` : asset.kind === "text" || asset.kind === "subtitle" ? `${require_notify.formatResolution(asset.width, asset.height)}` : `${require_notify.formatResolution(asset.width, asset.height)} · ${require_notify.formatBytes(asset.size)}`, asset.hasAudio ? " · 含音轨" : ""]
|
|
1208
1349
|
})]
|
|
1209
1350
|
}),
|
|
1210
1351
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DropdownMenu, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuTrigger, {
|
|
@@ -1246,7 +1387,7 @@ function MediaGrid({ assets, selectedAssetId, missingAssetIds = [], progress, on
|
|
|
1246
1387
|
className: "grid grid-cols-2 gap-2.5 p-2.5",
|
|
1247
1388
|
children: [
|
|
1248
1389
|
progress.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1249
|
-
className:
|
|
1390
|
+
className: require_notify.cn("flex flex-col gap-1.5 rounded-lg border border-dashed p-1.5", item.error != null && "border-destructive/50 bg-destructive/5"),
|
|
1250
1391
|
children: [
|
|
1251
1392
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Skeleton, { className: "aspect-video w-full rounded-md" }),
|
|
1252
1393
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -1297,107 +1438,6 @@ function MediaGrid({ assets, selectedAssetId, missingAssetIds = [], progress, on
|
|
|
1297
1438
|
]
|
|
1298
1439
|
});
|
|
1299
1440
|
}
|
|
1300
|
-
// src/features/media-library/hooks/useMediaImport.ts
|
|
1301
|
-
/**
|
|
1302
|
-
* 找出可以「重连」的素材。
|
|
1303
|
-
*
|
|
1304
|
-
* 场景:刷新后工程从 IndexedDB 恢复,但素材二进制消失(File 无法持久化)。
|
|
1305
|
-
* 用户重新导入同一个文件时,复用原 assetId,时间轴上已有片段立即恢复。
|
|
1306
|
-
* 匹配条件:同名 + 同类型 + 当前确实缺少二进制。
|
|
1307
|
-
*/
|
|
1308
|
-
function findReconnectableAssetId(state, file, registry) {
|
|
1309
|
-
if (state.missingAssetIds.length === 0) return null;
|
|
1310
|
-
return state.assets.find((asset) => state.missingAssetIds.includes(asset.id) && asset.name === file.name && registry.getFile(asset.id) == null)?.id ?? null;
|
|
1311
|
-
}
|
|
1312
|
-
/**
|
|
1313
|
-
* 批量导入素材。
|
|
1314
|
-
*
|
|
1315
|
-
* - 串行处理,避免同时开辟多个解码器把内存打爆;
|
|
1316
|
-
* - 单个文件失败只影响自身,最后汇总 toast;
|
|
1317
|
-
* - 进度写在本地 state(不放进 store,避免高频更新引起全局重渲染);
|
|
1318
|
-
* - 若与「缺失源文件」的素材同名同类型,则重连而不是新增。
|
|
1319
|
-
*/
|
|
1320
|
-
function useMediaImport() {
|
|
1321
|
-
const editor = (0, _cut_workbench_react.useCutEditor)();
|
|
1322
|
-
const clips = editor.runtime.clips;
|
|
1323
|
-
const store = editor.store;
|
|
1324
|
-
const [progress, setProgress] = (0, react.useState)([]);
|
|
1325
|
-
const [importing, setImporting] = (0, react.useState)(false);
|
|
1326
|
-
const addAsset = (0, _cut_workbench_react.useEditor)((s) => s.addAsset);
|
|
1327
|
-
const replaceAsset = (0, _cut_workbench_react.useEditor)((s) => s.replaceAsset);
|
|
1328
|
-
const clearMissing = (0, _cut_workbench_react.useEditor)((s) => s.clearMissing);
|
|
1329
|
-
const selectAsset = (0, _cut_workbench_react.useEditor)((s) => s.selectAsset);
|
|
1330
|
-
const beginImport = useUi((s) => s.beginImport);
|
|
1331
|
-
const endImport = useUi((s) => s.endImport);
|
|
1332
|
-
return {
|
|
1333
|
-
importing,
|
|
1334
|
-
progress,
|
|
1335
|
-
importFiles: (0, react.useCallback)(async (files) => {
|
|
1336
|
-
if (files.length === 0) return [];
|
|
1337
|
-
setImporting(true);
|
|
1338
|
-
beginImport();
|
|
1339
|
-
setProgress(files.map((f) => ({
|
|
1340
|
-
name: f.name,
|
|
1341
|
-
progress: 0
|
|
1342
|
-
})));
|
|
1343
|
-
const imported = [];
|
|
1344
|
-
const failures = [];
|
|
1345
|
-
let reconnected = 0;
|
|
1346
|
-
for (let i = 0; i < files.length; i += 1) {
|
|
1347
|
-
const file = files[i];
|
|
1348
|
-
if (!file) continue;
|
|
1349
|
-
try {
|
|
1350
|
-
const reconnectId = findReconnectableAssetId(store.getState(), file, clips);
|
|
1351
|
-
const result = await (0, _cut_workbench_core_engine.importFile)(clips, file, (p) => {
|
|
1352
|
-
setProgress((prev) => prev.map((item, index) => index === i ? {
|
|
1353
|
-
...item,
|
|
1354
|
-
progress: p
|
|
1355
|
-
} : item));
|
|
1356
|
-
});
|
|
1357
|
-
if (reconnectId != null) {
|
|
1358
|
-
clips.replaceAssetSource(reconnectId, file, result.master ?? null);
|
|
1359
|
-
replaceAsset(reconnectId, result.asset);
|
|
1360
|
-
clearMissing(reconnectId);
|
|
1361
|
-
imported.push({
|
|
1362
|
-
...result.asset,
|
|
1363
|
-
id: reconnectId
|
|
1364
|
-
});
|
|
1365
|
-
reconnected += 1;
|
|
1366
|
-
} else {
|
|
1367
|
-
addAsset(result.asset);
|
|
1368
|
-
imported.push(result.asset);
|
|
1369
|
-
}
|
|
1370
|
-
} catch (err) {
|
|
1371
|
-
const message = err instanceof _cut_workbench_core_engine.UnsupportedFileError ? err.message : err instanceof Error ? err.message : String(err);
|
|
1372
|
-
failures.push(`${file.name}:${message}`);
|
|
1373
|
-
setProgress((prev) => prev.map((item, index) => index === i ? {
|
|
1374
|
-
...item,
|
|
1375
|
-
error: message
|
|
1376
|
-
} : item));
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
setImporting(false);
|
|
1380
|
-
endImport();
|
|
1381
|
-
if (imported.length > 0) {
|
|
1382
|
-
selectAsset(imported[0]?.id ?? null);
|
|
1383
|
-
sonner.toast.success(reconnected > 0 ? `已导入 ${imported.length} 个素材(其中 ${reconnected} 个重连到已有片段)` : `已导入 ${imported.length} 个素材`);
|
|
1384
|
-
}
|
|
1385
|
-
if (failures.length > 0) sonner.toast.error(`${failures.length} 个文件导入失败`, {
|
|
1386
|
-
description: failures.slice(0, 3).join("\n"),
|
|
1387
|
-
duration: 8e3
|
|
1388
|
-
});
|
|
1389
|
-
window.setTimeout(() => setProgress([]), 3e3);
|
|
1390
|
-
return imported;
|
|
1391
|
-
}, [
|
|
1392
|
-
addAsset,
|
|
1393
|
-
beginImport,
|
|
1394
|
-
clearMissing,
|
|
1395
|
-
endImport,
|
|
1396
|
-
replaceAsset,
|
|
1397
|
-
selectAsset
|
|
1398
|
-
])
|
|
1399
|
-
};
|
|
1400
|
-
}
|
|
1401
1441
|
// src/features/media-library/components/MediaLibraryPanel.tsx
|
|
1402
1442
|
var KIND_LABEL$1 = {
|
|
1403
1443
|
video: "视频",
|
|
@@ -1485,11 +1525,11 @@ function MediaLibraryPanel({ onAddToTimeline, onRemoveAsset }) {
|
|
|
1485
1525
|
}),
|
|
1486
1526
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1487
1527
|
label: "分辨率",
|
|
1488
|
-
value:
|
|
1528
|
+
value: require_notify.formatResolution(metaAsset.width, metaAsset.height)
|
|
1489
1529
|
}),
|
|
1490
1530
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1491
1531
|
label: "大小",
|
|
1492
|
-
value: metaAsset.kind === "text" ? "—" :
|
|
1532
|
+
value: metaAsset.kind === "text" ? "—" : require_notify.formatBytes(metaAsset.size)
|
|
1493
1533
|
}),
|
|
1494
1534
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1495
1535
|
label: "音轨",
|
|
@@ -1565,6 +1605,7 @@ function CreateTextDialog({ open, onOpenChange, onCreated }) {
|
|
|
1565
1605
|
const projectWidth = (0, _cut_workbench_react.useEditor)((s) => s.project.width);
|
|
1566
1606
|
const projectHeight = (0, _cut_workbench_react.useEditor)((s) => s.project.height);
|
|
1567
1607
|
const addAsset = (0, _cut_workbench_react.useEditor)((s) => s.addAsset);
|
|
1608
|
+
const { bridge } = require_notify.useWorkbench();
|
|
1568
1609
|
const handleCreate = () => {
|
|
1569
1610
|
const { asset } = (0, _cut_workbench_core_engine.createTextAsset)({
|
|
1570
1611
|
registry: editor.runtime.clips,
|
|
@@ -1575,7 +1616,10 @@ function CreateTextDialog({ open, onOpenChange, onCreated }) {
|
|
|
1575
1616
|
addAsset(asset);
|
|
1576
1617
|
onCreated(asset.id);
|
|
1577
1618
|
onOpenChange(false);
|
|
1578
|
-
|
|
1619
|
+
require_notify.notify(bridge, {
|
|
1620
|
+
level: "success",
|
|
1621
|
+
title: "已创建文字素材"
|
|
1622
|
+
});
|
|
1579
1623
|
};
|
|
1580
1624
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dialog, {
|
|
1581
1625
|
open,
|
|
@@ -1609,10 +1653,15 @@ function CreateSubtitleDialog({ open, onOpenChange, onCreated }) {
|
|
|
1609
1653
|
const projectWidth = (0, _cut_workbench_react.useEditor)((s) => s.project.width);
|
|
1610
1654
|
const projectHeight = (0, _cut_workbench_react.useEditor)((s) => s.project.height);
|
|
1611
1655
|
const addAsset = (0, _cut_workbench_react.useEditor)((s) => s.addAsset);
|
|
1656
|
+
const { bridge } = require_notify.useWorkbench();
|
|
1612
1657
|
const handleCreate = () => {
|
|
1613
1658
|
const durationUs = (0, _cut_workbench_core.parseSrtDurationUs)(srt);
|
|
1614
1659
|
if (durationUs <= 0) {
|
|
1615
|
-
|
|
1660
|
+
require_notify.notify(bridge, {
|
|
1661
|
+
level: "error",
|
|
1662
|
+
title: "SRT 解析失败",
|
|
1663
|
+
description: "请检查时间轴格式:00:00:00,000 --> 00:00:02,000"
|
|
1664
|
+
});
|
|
1616
1665
|
return;
|
|
1617
1666
|
}
|
|
1618
1667
|
const { asset } = (0, _cut_workbench_core_engine.createSubtitleAsset)({
|
|
@@ -1625,7 +1674,10 @@ function CreateSubtitleDialog({ open, onOpenChange, onCreated }) {
|
|
|
1625
1674
|
addAsset(asset);
|
|
1626
1675
|
onCreated(asset.id);
|
|
1627
1676
|
onOpenChange(false);
|
|
1628
|
-
|
|
1677
|
+
require_notify.notify(bridge, {
|
|
1678
|
+
level: "success",
|
|
1679
|
+
title: "已创建字幕素材"
|
|
1680
|
+
});
|
|
1629
1681
|
};
|
|
1630
1682
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dialog, {
|
|
1631
1683
|
open,
|
|
@@ -1651,11 +1703,22 @@ function CreateSubtitleDialog({ open, onOpenChange, onCreated }) {
|
|
|
1651
1703
|
});
|
|
1652
1704
|
}
|
|
1653
1705
|
// src/hooks/use-toast-safe.ts
|
|
1654
|
-
/**
|
|
1706
|
+
/**
|
|
1707
|
+
* 把异常转成可见提示,禁止静默吞掉。
|
|
1708
|
+
*
|
|
1709
|
+
* 这是**非 hook** 版本,供事件回调 / 非组件代码调用(例如 `useAvCanvas` 的
|
|
1710
|
+
* 初始化失败)。它拿不到 React context,因此走 `notify` 注册的默认桥:
|
|
1711
|
+
* `WorkbenchProvider` 挂载时会把 `bridge` 注册进来,宿主实现了 `notify`
|
|
1712
|
+
* 就交给宿主,否则退回库内 toast。
|
|
1713
|
+
*/
|
|
1655
1714
|
function reportError(error, fallback = "操作失败") {
|
|
1656
1715
|
const message = error instanceof Error ? error.message : String(error);
|
|
1657
1716
|
console.error(error);
|
|
1658
|
-
|
|
1717
|
+
require_notify.notify(void 0, {
|
|
1718
|
+
level: "error",
|
|
1719
|
+
title: fallback,
|
|
1720
|
+
description: message
|
|
1721
|
+
});
|
|
1659
1722
|
}
|
|
1660
1723
|
// src/features/preview/hooks/useAvCanvas.ts
|
|
1661
1724
|
/**
|
|
@@ -2285,7 +2348,7 @@ function PreviewStage({ attachRef, width, height, emptyHint, onCapture, onFullsc
|
|
|
2285
2348
|
const scaleY = height > 0 ? box.h / height : 1;
|
|
2286
2349
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2287
2350
|
ref: outerRef,
|
|
2288
|
-
className:
|
|
2351
|
+
className: require_notify.cn("relative flex min-h-0 flex-1 items-center justify-center overflow-hidden bg-stage-bg", className),
|
|
2289
2352
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2290
2353
|
className: "relative shrink-0 overflow-hidden rounded-md bg-black shadow-2xl ring-1 ring-border",
|
|
2291
2354
|
style: {
|
|
@@ -2346,14 +2409,14 @@ function Slider({ className, defaultValue, value, min = 0, max = 100, ...props }
|
|
|
2346
2409
|
value,
|
|
2347
2410
|
min,
|
|
2348
2411
|
max,
|
|
2349
|
-
className:
|
|
2412
|
+
className: require_notify.cn("relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col", className),
|
|
2350
2413
|
...props,
|
|
2351
2414
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Slider.Track, {
|
|
2352
2415
|
"data-slot": "slider-track",
|
|
2353
|
-
className:
|
|
2416
|
+
className: require_notify.cn("relative grow overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"),
|
|
2354
2417
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Slider.Range, {
|
|
2355
2418
|
"data-slot": "slider-range",
|
|
2356
|
-
className:
|
|
2419
|
+
className: require_notify.cn("absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full")
|
|
2357
2420
|
})
|
|
2358
2421
|
}), Array.from({ length: _values.length }, (_, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Slider.Thumb, {
|
|
2359
2422
|
"data-slot": "slider-thumb",
|
|
@@ -2368,7 +2431,7 @@ function Slider({ className, defaultValue, value, min = 0, max = 100, ...props }
|
|
|
2368
2431
|
*/
|
|
2369
2432
|
function Timecode({ us, showMs = true, className, muted, title }) {
|
|
2370
2433
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2371
|
-
className:
|
|
2434
|
+
className: require_notify.cn("font-mono text-xs tabular-nums", muted && "text-muted-foreground", className),
|
|
2372
2435
|
title: title ?? `${us} µs`,
|
|
2373
2436
|
children: (0, _cut_workbench_core.formatTimecode)(us, showMs)
|
|
2374
2437
|
});
|
|
@@ -2419,7 +2482,7 @@ function PlaybackControls({ playing, currentUs, durationUs, masterVolume, playba
|
|
|
2419
2482
|
const full = width === 0 || width >= FULL_PX;
|
|
2420
2483
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2421
2484
|
ref: rootRef,
|
|
2422
|
-
className:
|
|
2485
|
+
className: require_notify.cn("flex h-11 shrink-0 items-center gap-1.5 overflow-hidden border-t bg-card/60", ultraCompact ? "px-1.5" : "px-3", className),
|
|
2423
2486
|
children: [
|
|
2424
2487
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2425
2488
|
className: "flex shrink-0 items-center gap-0.5",
|
|
@@ -2480,7 +2543,7 @@ function PlaybackControls({ playing, currentUs, durationUs, masterVolume, playba
|
|
|
2480
2543
|
]
|
|
2481
2544
|
}),
|
|
2482
2545
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2483
|
-
className:
|
|
2546
|
+
className: require_notify.cn("mx-2 flex shrink-0 items-baseline gap-1 whitespace-nowrap", ultraCompact && "mx-1", micro && "hidden"),
|
|
2484
2547
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timecode, {
|
|
2485
2548
|
us: currentUs,
|
|
2486
2549
|
className: "text-[13px] font-medium"
|
|
@@ -2504,14 +2567,14 @@ function PlaybackControls({ playing, currentUs, durationUs, masterVolume, playba
|
|
|
2504
2567
|
"aria-label": "播放进度"
|
|
2505
2568
|
}),
|
|
2506
2569
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2507
|
-
className:
|
|
2570
|
+
className: require_notify.cn("flex shrink-0 items-center gap-1", compact && "ml-auto", micro && "hidden"),
|
|
2508
2571
|
children: [
|
|
2509
2572
|
!compact && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2510
2573
|
className: "flex items-center rounded-md border p-0.5",
|
|
2511
2574
|
children: RATES.map((rate) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2512
2575
|
type: "button",
|
|
2513
2576
|
onClick: () => onRateChange(rate),
|
|
2514
|
-
className:
|
|
2577
|
+
className: require_notify.cn("rounded px-1.5 py-0.5 text-[11px] tabular-nums transition-colors", playbackRate === rate ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground"),
|
|
2515
2578
|
title: rate !== 1 ? "变速预览(不会改变导出结果)" : "正常速度",
|
|
2516
2579
|
children: [rate, "×"]
|
|
2517
2580
|
}, rate))
|
|
@@ -2575,6 +2638,7 @@ function PreviewPanel() {
|
|
|
2575
2638
|
const setMasterVolume = (0, _cut_workbench_react.useEditor)((s) => s.setMasterVolume);
|
|
2576
2639
|
const playheadUs = (0, _cut_workbench_react.useEditor)((s) => s.playheadUs);
|
|
2577
2640
|
const selectedClipIds = (0, _cut_workbench_react.useEditor)((s) => s.selectedClipIds);
|
|
2641
|
+
const { bridge } = require_notify.useWorkbench();
|
|
2578
2642
|
const canvas = useAvCanvas({
|
|
2579
2643
|
width: project.width,
|
|
2580
2644
|
height: project.height
|
|
@@ -2585,12 +2649,18 @@ function PreviewPanel() {
|
|
|
2585
2649
|
if (errors.length === 0) return;
|
|
2586
2650
|
const missing = errors.filter((e) => e.message.startsWith("素材文件未载入"));
|
|
2587
2651
|
const other = errors.filter((e) => !e.message.startsWith("素材文件未载入"));
|
|
2588
|
-
if (missing.length > 0)
|
|
2652
|
+
if (missing.length > 0) require_notify.notify(bridge, {
|
|
2653
|
+
level: "warning",
|
|
2654
|
+
title: `${missing.length} 个片段缺少源文件,已跳过预览`,
|
|
2589
2655
|
description: "刷新后素材二进制不会保留,请重新导入相同文件",
|
|
2590
2656
|
duration: 8e3
|
|
2591
2657
|
});
|
|
2592
|
-
if (other.length > 0)
|
|
2593
|
-
|
|
2658
|
+
if (other.length > 0) require_notify.notify(bridge, {
|
|
2659
|
+
level: "error",
|
|
2660
|
+
title: "部分片段无法载入预览",
|
|
2661
|
+
description: Array.from(new Set(other.map((e) => e.message))).slice(0, 3).join("\n")
|
|
2662
|
+
});
|
|
2663
|
+
}, [bridge]);
|
|
2594
2664
|
const clock = usePlaybackClock({
|
|
2595
2665
|
project,
|
|
2596
2666
|
assets,
|
|
@@ -2726,12 +2796,18 @@ function PreviewPanel() {
|
|
|
2726
2796
|
const handleCapture = (0, react.useCallback)(() => {
|
|
2727
2797
|
const dataUrl = canvas.captureImage();
|
|
2728
2798
|
if (dataUrl == null) {
|
|
2729
|
-
|
|
2799
|
+
require_notify.notify(bridge, {
|
|
2800
|
+
level: "error",
|
|
2801
|
+
title: "截图失败:画布尚未就绪"
|
|
2802
|
+
});
|
|
2730
2803
|
return;
|
|
2731
2804
|
}
|
|
2732
2805
|
downloadDataUrl(dataUrl, `frame-${Date.now()}.png`);
|
|
2733
|
-
|
|
2734
|
-
|
|
2806
|
+
require_notify.notify(bridge, {
|
|
2807
|
+
level: "success",
|
|
2808
|
+
title: "已导出当前帧"
|
|
2809
|
+
});
|
|
2810
|
+
}, [bridge, canvas]);
|
|
2735
2811
|
const handleFullscreen = () => {
|
|
2736
2812
|
const el = canvas.containerRef.current?.parentElement;
|
|
2737
2813
|
if (el == null) return;
|
|
@@ -2968,14 +3044,14 @@ function Ruler({ pxPerSecond, widthPx, playheadPx, scrollLeftPx, className }) {
|
|
|
2968
3044
|
widthPx
|
|
2969
3045
|
]);
|
|
2970
3046
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2971
|
-
className:
|
|
3047
|
+
className: require_notify.cn("relative shrink-0 cursor-ew-resize border-b bg-timeline-bg select-none", className),
|
|
2972
3048
|
style: {
|
|
2973
3049
|
height: 28,
|
|
2974
3050
|
width: widthPx
|
|
2975
3051
|
},
|
|
2976
3052
|
children: [
|
|
2977
3053
|
ticks.map((tick) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2978
|
-
className:
|
|
3054
|
+
className: require_notify.cn("absolute bottom-0 w-px", tick.major ? "h-2 bg-muted-foreground/50" : "h-1 bg-muted-foreground/25"),
|
|
2979
3055
|
style: { left: tick.px }
|
|
2980
3056
|
}, `${tick.us}-${tick.major ? "M" : "m"}`)),
|
|
2981
3057
|
ticks.filter((t) => t.major).map((tick) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -3003,7 +3079,7 @@ function Ruler({ pxPerSecond, widthPx, playheadPx, scrollLeftPx, className }) {
|
|
|
3003
3079
|
*/
|
|
3004
3080
|
var TrackHeader = (0, react.memo)(function TrackHeader({ track, onToggleMute, onToggleLock, onToggleHidden, onRemove }) {
|
|
3005
3081
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3006
|
-
className:
|
|
3082
|
+
className: require_notify.cn("flex items-center gap-1 border-r border-b border-timeline-grid px-2", track.kind === "audio" ? "bg-clip-audio/10" : "bg-clip-video/10", track.hidden && "opacity-60"),
|
|
3007
3083
|
style: { height: 56 },
|
|
3008
3084
|
"data-track-header": true,
|
|
3009
3085
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -3058,7 +3134,7 @@ function IconToggle({ active, onClick, label, icon: Icon }) {
|
|
|
3058
3134
|
onClick,
|
|
3059
3135
|
"aria-label": label,
|
|
3060
3136
|
"aria-pressed": active,
|
|
3061
|
-
className:
|
|
3137
|
+
className: require_notify.cn("flex size-[18px] items-center justify-center rounded transition-colors", active ? "bg-primary/20 text-primary" : "text-muted-foreground hover:text-foreground"),
|
|
3062
3138
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: "size-3" })
|
|
3063
3139
|
})
|
|
3064
3140
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: label })] });
|
|
@@ -3149,7 +3225,7 @@ var ClipBlock = (0, react.memo)(function ClipBlock({ clip, asset, pxPerSecond, s
|
|
|
3149
3225
|
handlers.beginMove(event, clip);
|
|
3150
3226
|
},
|
|
3151
3227
|
onDoubleClick: () => handlers.seekTo(clip),
|
|
3152
|
-
className:
|
|
3228
|
+
className: require_notify.cn("group absolute top-1 bottom-1 overflow-hidden rounded-md border text-left select-none transition-shadow", KIND_BG[kind], selected ? "ring-2 ring-primary ring-offset-1 ring-offset-timeline-bg" : "hover:brightness-110", locked ? "cursor-not-allowed opacity-70" : "cursor-grab active:cursor-grabbing"),
|
|
3153
3229
|
style: {
|
|
3154
3230
|
left: leftPx,
|
|
3155
3231
|
width: widthPx,
|
|
@@ -3228,7 +3304,7 @@ function ContextMenuPortal({ ...props }) {
|
|
|
3228
3304
|
function ContextMenuContent({ className, ...props }) {
|
|
3229
3305
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextMenuPortal, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ContextMenu.Content, {
|
|
3230
3306
|
"data-slot": "context-menu-content",
|
|
3231
|
-
className:
|
|
3307
|
+
className: require_notify.cn("z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", className),
|
|
3232
3308
|
...props
|
|
3233
3309
|
}) });
|
|
3234
3310
|
}
|
|
@@ -3237,21 +3313,21 @@ function ContextMenuItem({ className, inset, variant = "default", ...props }) {
|
|
|
3237
3313
|
"data-slot": "context-menu-item",
|
|
3238
3314
|
"data-inset": inset,
|
|
3239
3315
|
"data-variant": variant,
|
|
3240
|
-
className:
|
|
3316
|
+
className: require_notify.cn("relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!", className),
|
|
3241
3317
|
...props
|
|
3242
3318
|
});
|
|
3243
3319
|
}
|
|
3244
3320
|
function ContextMenuSeparator({ className, ...props }) {
|
|
3245
3321
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ContextMenu.Separator, {
|
|
3246
3322
|
"data-slot": "context-menu-separator",
|
|
3247
|
-
className:
|
|
3323
|
+
className: require_notify.cn("-mx-1 my-1 h-px bg-border", className),
|
|
3248
3324
|
...props
|
|
3249
3325
|
});
|
|
3250
3326
|
}
|
|
3251
3327
|
function ContextMenuShortcut({ className, ...props }) {
|
|
3252
3328
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3253
3329
|
"data-slot": "context-menu-shortcut",
|
|
3254
|
-
className:
|
|
3330
|
+
className: require_notify.cn("ml-auto text-xs tracking-widest text-muted-foreground", className),
|
|
3255
3331
|
...props
|
|
3256
3332
|
});
|
|
3257
3333
|
}
|
|
@@ -3308,14 +3384,14 @@ function ClipContextMenu({ children, onSplit, onDuplicate, onRemove, onShowInfo,
|
|
|
3308
3384
|
/** 播放头:一条竖线 + 顶部把手,跨过所有轨道 */
|
|
3309
3385
|
function Playhead({ px, playing, className }) {
|
|
3310
3386
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3311
|
-
className:
|
|
3387
|
+
className: require_notify.cn("pointer-events-none absolute top-0 bottom-0 z-20 w-px", className),
|
|
3312
3388
|
style: { left: px },
|
|
3313
3389
|
"aria-hidden": true,
|
|
3314
3390
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3315
|
-
className:
|
|
3391
|
+
className: require_notify.cn("absolute inset-y-0 -left-px w-0.5 bg-playhead", playing && "shadow-[0_0_6px_var(--cw-playhead)]"),
|
|
3316
3392
|
style: { top: 24 }
|
|
3317
3393
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3318
|
-
className:
|
|
3394
|
+
className: require_notify.cn("absolute -translate-x-1/2 rounded-sm bg-playhead", playing ? "size-3 rotate-45" : "h-2.5 w-2.5 rotate-45"),
|
|
3319
3395
|
style: { top: 14 }
|
|
3320
3396
|
})]
|
|
3321
3397
|
});
|
|
@@ -3324,7 +3400,7 @@ function Playhead({ px, playing, className }) {
|
|
|
3324
3400
|
/** 框选矩形 */
|
|
3325
3401
|
function Marquee({ x1, y1, x2, y2, className }) {
|
|
3326
3402
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3327
|
-
className:
|
|
3403
|
+
className: require_notify.cn("pointer-events-none absolute z-40 rounded-sm border border-primary/70 bg-primary/15", className),
|
|
3328
3404
|
style: {
|
|
3329
3405
|
left: Math.min(x1, x2),
|
|
3330
3406
|
top: Math.min(y1, y2),
|
|
@@ -3342,10 +3418,10 @@ function Marquee({ x1, y1, x2, y2, className }) {
|
|
|
3342
3418
|
* 调整回去,视觉上内容不会乱跳。
|
|
3343
3419
|
*/
|
|
3344
3420
|
function useTimelineZoom(containerRef) {
|
|
3345
|
-
const uiApi = useUiStoreApi();
|
|
3346
|
-
const pxPerSecond = useUi((s) => s.pxPerSecond);
|
|
3347
|
-
const setPxPerSecond = useUi((s) => s.setPxPerSecond);
|
|
3348
|
-
const setScrollLeftPx = useUi((s) => s.setScrollLeftPx);
|
|
3421
|
+
const uiApi = require_notify.useUiStoreApi();
|
|
3422
|
+
const pxPerSecond = require_notify.useUi((s) => s.pxPerSecond);
|
|
3423
|
+
const setPxPerSecond = require_notify.useUi((s) => s.setPxPerSecond);
|
|
3424
|
+
const setScrollLeftPx = require_notify.useUi((s) => s.setScrollLeftPx);
|
|
3349
3425
|
const applyZoom = (0, react.useCallback)((nextPps, anchorClientX) => {
|
|
3350
3426
|
const container = containerRef.current;
|
|
3351
3427
|
const prevPps = uiApi.getState().pxPerSecond;
|
|
@@ -3670,7 +3746,7 @@ function useTimelineDrag({ pxPerSecond, contentLeftPx, resolveTrackAt, snapEnabl
|
|
|
3670
3746
|
*/
|
|
3671
3747
|
function Timeline() {
|
|
3672
3748
|
const editor = (0, _cut_workbench_react.useCutEditor)();
|
|
3673
|
-
const uiApi = useUiStoreApi();
|
|
3749
|
+
const uiApi = require_notify.useUiStoreApi();
|
|
3674
3750
|
const project = (0, _cut_workbench_react.useEditor)((s) => s.project);
|
|
3675
3751
|
const assets = (0, _cut_workbench_react.useEditor)((s) => s.assets);
|
|
3676
3752
|
const selectedClipIds = (0, _cut_workbench_react.useEditor)((s) => s.selectedClipIds);
|
|
@@ -3999,7 +4075,7 @@ function Timeline() {
|
|
|
3999
4075
|
"data-timeline-tracks": true,
|
|
4000
4076
|
children: [
|
|
4001
4077
|
project.tracks.map((track, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4002
|
-
className:
|
|
4078
|
+
className: require_notify.cn("relative border-b border-timeline-grid", track.kind === "audio" ? "bg-timeline-track/30" : "bg-timeline-track/50", track.hidden && "opacity-40"),
|
|
4003
4079
|
style: { height: 56 },
|
|
4004
4080
|
"data-track-row": track.id,
|
|
4005
4081
|
children: [
|
|
@@ -4070,7 +4146,7 @@ function Tabs({ className, orientation = "horizontal", ...props }) {
|
|
|
4070
4146
|
"data-slot": "tabs",
|
|
4071
4147
|
"data-orientation": orientation,
|
|
4072
4148
|
orientation,
|
|
4073
|
-
className:
|
|
4149
|
+
className: require_notify.cn("group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", className),
|
|
4074
4150
|
...props
|
|
4075
4151
|
});
|
|
4076
4152
|
}
|
|
@@ -4085,21 +4161,21 @@ function TabsList({ className, variant = "default", ...props }) {
|
|
|
4085
4161
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Tabs.List, {
|
|
4086
4162
|
"data-slot": "tabs-list",
|
|
4087
4163
|
"data-variant": variant,
|
|
4088
|
-
className:
|
|
4164
|
+
className: require_notify.cn(tabsListVariants({ variant }), className),
|
|
4089
4165
|
...props
|
|
4090
4166
|
});
|
|
4091
4167
|
}
|
|
4092
4168
|
function TabsTrigger({ className, ...props }) {
|
|
4093
4169
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Tabs.Trigger, {
|
|
4094
4170
|
"data-slot": "tabs-trigger",
|
|
4095
|
-
className:
|
|
4171
|
+
className: require_notify.cn("relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent", "data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground", "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100", className),
|
|
4096
4172
|
...props
|
|
4097
4173
|
});
|
|
4098
4174
|
}
|
|
4099
4175
|
function TabsContent({ className, ...props }) {
|
|
4100
4176
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Tabs.Content, {
|
|
4101
4177
|
"data-slot": "tabs-content",
|
|
4102
|
-
className:
|
|
4178
|
+
className: require_notify.cn("flex-1 outline-none", className),
|
|
4103
4179
|
...props
|
|
4104
4180
|
});
|
|
4105
4181
|
}
|
|
@@ -4108,7 +4184,7 @@ function Input({ className, type, ...props }) {
|
|
|
4108
4184
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4109
4185
|
type,
|
|
4110
4186
|
"data-slot": "input",
|
|
4111
|
-
className:
|
|
4187
|
+
className: require_notify.cn("h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30", "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40", className),
|
|
4112
4188
|
...props
|
|
4113
4189
|
});
|
|
4114
4190
|
}
|
|
@@ -4129,7 +4205,7 @@ function SelectTrigger({ className, size = "default", children, ...props }) {
|
|
|
4129
4205
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Select.Trigger, {
|
|
4130
4206
|
"data-slot": "select-trigger",
|
|
4131
4207
|
"data-size": size,
|
|
4132
|
-
className:
|
|
4208
|
+
className: require_notify.cn("flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground", className),
|
|
4133
4209
|
...props,
|
|
4134
4210
|
children: [children, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Select.Icon, {
|
|
4135
4211
|
asChild: true,
|
|
@@ -4143,14 +4219,14 @@ function SelectContent({ className, children, position = "item-aligned", align =
|
|
|
4143
4219
|
container: container ?? void 0,
|
|
4144
4220
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Select.Content, {
|
|
4145
4221
|
"data-slot": "select-content",
|
|
4146
|
-
className:
|
|
4222
|
+
className: require_notify.cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className),
|
|
4147
4223
|
position,
|
|
4148
4224
|
align,
|
|
4149
4225
|
...props,
|
|
4150
4226
|
children: [
|
|
4151
4227
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectScrollUpButton, {}),
|
|
4152
4228
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Select.Viewport, {
|
|
4153
|
-
className:
|
|
4229
|
+
className: require_notify.cn("p-1", position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"),
|
|
4154
4230
|
children
|
|
4155
4231
|
}),
|
|
4156
4232
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectScrollDownButton, {})
|
|
@@ -4161,7 +4237,7 @@ function SelectContent({ className, children, position = "item-aligned", align =
|
|
|
4161
4237
|
function SelectItem({ className, children, ...props }) {
|
|
4162
4238
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Select.Item, {
|
|
4163
4239
|
"data-slot": "select-item",
|
|
4164
|
-
className:
|
|
4240
|
+
className: require_notify.cn("relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", className),
|
|
4165
4241
|
...props,
|
|
4166
4242
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4167
4243
|
"data-slot": "select-item-indicator",
|
|
@@ -4173,7 +4249,7 @@ function SelectItem({ className, children, ...props }) {
|
|
|
4173
4249
|
function SelectScrollUpButton({ className, ...props }) {
|
|
4174
4250
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Select.ScrollUpButton, {
|
|
4175
4251
|
"data-slot": "select-scroll-up-button",
|
|
4176
|
-
className:
|
|
4252
|
+
className: require_notify.cn("flex cursor-default items-center justify-center py-1", className),
|
|
4177
4253
|
...props,
|
|
4178
4254
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronUpIcon, { className: "size-4" })
|
|
4179
4255
|
});
|
|
@@ -4181,7 +4257,7 @@ function SelectScrollUpButton({ className, ...props }) {
|
|
|
4181
4257
|
function SelectScrollDownButton({ className, ...props }) {
|
|
4182
4258
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Select.ScrollDownButton, {
|
|
4183
4259
|
"data-slot": "select-scroll-down-button",
|
|
4184
|
-
className:
|
|
4260
|
+
className: require_notify.cn("flex cursor-default items-center justify-center py-1", className),
|
|
4185
4261
|
...props,
|
|
4186
4262
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDownIcon, { className: "size-4" })
|
|
4187
4263
|
});
|
|
@@ -4190,7 +4266,7 @@ function SelectScrollDownButton({ className, ...props }) {
|
|
|
4190
4266
|
/** 属性面板的一行:标签 + 控件(标签在左,控件占满剩余宽度) */
|
|
4191
4267
|
function Field({ label, hint, children, className, htmlFor }) {
|
|
4192
4268
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4193
|
-
className:
|
|
4269
|
+
className: require_notify.cn("flex items-center gap-2", className),
|
|
4194
4270
|
children: [
|
|
4195
4271
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Label, {
|
|
4196
4272
|
htmlFor,
|
|
@@ -4211,7 +4287,7 @@ function Field({ label, hint, children, className, htmlFor }) {
|
|
|
4211
4287
|
/** 属性分组 */
|
|
4212
4288
|
function Section({ title, children, action, className }) {
|
|
4213
4289
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
4214
|
-
className:
|
|
4290
|
+
className: require_notify.cn("space-y-2", className),
|
|
4215
4291
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4216
4292
|
className: "flex items-center justify-between",
|
|
4217
4293
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
@@ -4406,7 +4482,7 @@ function VideoProps({ clip, onPatch, onPatchTransform, onPatchSize }) {
|
|
|
4406
4482
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
|
|
4407
4483
|
type: "number",
|
|
4408
4484
|
className: "h-7 text-xs tabular-nums",
|
|
4409
|
-
value:
|
|
4485
|
+
value: require_notify.formatSeconds(clip.startUs),
|
|
4410
4486
|
step: .1,
|
|
4411
4487
|
min: 0,
|
|
4412
4488
|
onChange: (e) => {
|
|
@@ -4420,7 +4496,7 @@ function VideoProps({ clip, onPatch, onPatchTransform, onPatchSize }) {
|
|
|
4420
4496
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
|
|
4421
4497
|
type: "number",
|
|
4422
4498
|
className: "h-7 text-xs tabular-nums",
|
|
4423
|
-
value:
|
|
4499
|
+
value: require_notify.formatSeconds(clip.durationUs),
|
|
4424
4500
|
step: .1,
|
|
4425
4501
|
min: .1,
|
|
4426
4502
|
onChange: (e) => {
|
|
@@ -4527,7 +4603,7 @@ function AudioProps({ clip, onPatch }) {
|
|
|
4527
4603
|
className: "text-[10px] text-muted-foreground",
|
|
4528
4604
|
children: [
|
|
4529
4605
|
"片段时长 ",
|
|
4530
|
-
|
|
4606
|
+
require_notify.formatSeconds(clip.durationUs),
|
|
4531
4607
|
"s,淡入使用 setAnimation 线性插值。"
|
|
4532
4608
|
]
|
|
4533
4609
|
})
|
|
@@ -4782,7 +4858,7 @@ function SubtitleProps({ clip, onPatch, onRebuild }) {
|
|
|
4782
4858
|
className: "text-[10px] text-muted-foreground",
|
|
4783
4859
|
children: [
|
|
4784
4860
|
"片段时长 ",
|
|
4785
|
-
|
|
4861
|
+
require_notify.formatSeconds(clip.durationUs),
|
|
4786
4862
|
"s",
|
|
4787
4863
|
cues.length > 0 && ` · 最后一条结束于 ${(cues[cues.length - 1].endMs / 1e3).toFixed(2)}s`
|
|
4788
4864
|
]
|
|
@@ -5054,7 +5130,7 @@ function InspectorPanel() {
|
|
|
5054
5130
|
title: asset.name,
|
|
5055
5131
|
children: asset.name
|
|
5056
5132
|
}),
|
|
5057
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
5133
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_notify.Badge, {
|
|
5058
5134
|
variant: "outline",
|
|
5059
5135
|
className: "text-[10px]",
|
|
5060
5136
|
children: KIND_LABEL[asset.kind]
|
|
@@ -5077,7 +5153,7 @@ function InspectorPanel() {
|
|
|
5077
5153
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
5078
5154
|
(0, _cut_workbench_core.formatDuration)(clip.durationUs),
|
|
5079
5155
|
" · ",
|
|
5080
|
-
|
|
5156
|
+
require_notify.formatResolution(asset.width, asset.height)
|
|
5081
5157
|
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
5082
5158
|
"入点 ",
|
|
5083
5159
|
(clip.inPointUs / 1e6).toFixed(2),
|
|
@@ -5182,11 +5258,11 @@ function Switch({ className, size = "default", ...props }) {
|
|
|
5182
5258
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Switch.Root, {
|
|
5183
5259
|
"data-slot": "switch",
|
|
5184
5260
|
"data-size": size,
|
|
5185
|
-
className:
|
|
5261
|
+
className: require_notify.cn("peer group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80", className),
|
|
5186
5262
|
...props,
|
|
5187
5263
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Switch.Thumb, {
|
|
5188
5264
|
"data-slot": "switch-thumb",
|
|
5189
|
-
className:
|
|
5265
|
+
className: require_notify.cn("pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground")
|
|
5190
5266
|
})
|
|
5191
5267
|
});
|
|
5192
5268
|
}
|
|
@@ -5194,7 +5270,7 @@ function Switch({ className, size = "default", ...props }) {
|
|
|
5194
5270
|
function Progress({ className, value, ...props }) {
|
|
5195
5271
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Progress.Root, {
|
|
5196
5272
|
"data-slot": "progress",
|
|
5197
|
-
className:
|
|
5273
|
+
className: require_notify.cn("relative h-2 w-full overflow-hidden rounded-full bg-primary/20", className),
|
|
5198
5274
|
...props,
|
|
5199
5275
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Progress.Indicator, {
|
|
5200
5276
|
"data-slot": "progress-indicator",
|
|
@@ -5725,8 +5801,8 @@ var FPS_OPTIONS = [
|
|
|
5725
5801
|
* 所有重活都在 Worker 里;这里只负责 UI 与状态机。
|
|
5726
5802
|
*/
|
|
5727
5803
|
function ExportDialog() {
|
|
5728
|
-
const open = useUi((s) => s.dialog === "export");
|
|
5729
|
-
const closeDialog = useUi((s) => s.closeDialog);
|
|
5804
|
+
const open = require_notify.useUi((s) => s.dialog === "export");
|
|
5805
|
+
const closeDialog = require_notify.useUi((s) => s.closeDialog);
|
|
5730
5806
|
const project = (0, _cut_workbench_react.useEditor)((s) => s.project);
|
|
5731
5807
|
const assets = (0, _cut_workbench_react.useEditor)((s) => s.assets);
|
|
5732
5808
|
const [preset, setPreset] = (0, react.useState)("1080p");
|
|
@@ -5872,7 +5948,7 @@ function ExportDialog() {
|
|
|
5872
5948
|
className: "flex justify-between",
|
|
5873
5949
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "预估大小" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5874
5950
|
className: "font-mono tabular-nums",
|
|
5875
|
-
children:
|
|
5951
|
+
children: require_notify.formatBytes(estimatedBytes)
|
|
5876
5952
|
})]
|
|
5877
5953
|
}),
|
|
5878
5954
|
hasTextClips && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -5909,10 +5985,10 @@ function ExportDialog() {
|
|
|
5909
5985
|
children: [
|
|
5910
5986
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.CheckCircle2, { className: "size-3.5" }),
|
|
5911
5987
|
"导出完成 · 耗时 ",
|
|
5912
|
-
|
|
5988
|
+
require_notify.formatElapsed(state.result.elapsedMs / 1e3),
|
|
5913
5989
|
" ·",
|
|
5914
5990
|
" ",
|
|
5915
|
-
|
|
5991
|
+
require_notify.formatBytes(state.result.blob.size),
|
|
5916
5992
|
state.result.mode === "main" && "(主线程)"
|
|
5917
5993
|
]
|
|
5918
5994
|
}),
|
|
@@ -5975,7 +6051,7 @@ function ExportProgress({ progress, label = "正在合成…", className }) {
|
|
|
5975
6051
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Loader2, { className: "size-3 animate-spin" }), label]
|
|
5976
6052
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5977
6053
|
className: "font-mono tabular-nums",
|
|
5978
|
-
children:
|
|
6054
|
+
children: require_notify.formatPercent(progress)
|
|
5979
6055
|
})]
|
|
5980
6056
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Progress, { value: progress * 100 })]
|
|
5981
6057
|
});
|
|
@@ -6051,7 +6127,7 @@ function CanvasSettingsBody({ onDone }) {
|
|
|
6051
6127
|
setDraftW(String(preset.width));
|
|
6052
6128
|
setDraftH(String(preset.height));
|
|
6053
6129
|
},
|
|
6054
|
-
className:
|
|
6130
|
+
className: require_notify.cn("flex flex-col items-center gap-1 rounded-md border px-2 py-2 text-[11px] transition-colors", activePreset?.id === preset.id ? "border-primary bg-primary/10 text-foreground" : "text-muted-foreground hover:border-primary/50 hover:text-foreground"),
|
|
6055
6131
|
title: preset.hint,
|
|
6056
6132
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6057
6133
|
className: "block rounded-[2px] border border-current opacity-70",
|
|
@@ -6112,7 +6188,7 @@ function CanvasSettingsBody({ onDone }) {
|
|
|
6112
6188
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6113
6189
|
type: "button",
|
|
6114
6190
|
onClick: () => setMode("scale"),
|
|
6115
|
-
className:
|
|
6191
|
+
className: require_notify.cn("rounded-md border px-2.5 py-2 text-left text-[11px] transition-colors", mode === "scale" ? "border-primary bg-primary/10" : "text-muted-foreground hover:border-primary/50"),
|
|
6116
6192
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6117
6193
|
className: "block font-medium text-foreground",
|
|
6118
6194
|
children: "随画布缩放"
|
|
@@ -6123,7 +6199,7 @@ function CanvasSettingsBody({ onDone }) {
|
|
|
6123
6199
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6124
6200
|
type: "button",
|
|
6125
6201
|
onClick: () => setMode("keep"),
|
|
6126
|
-
className:
|
|
6202
|
+
className: require_notify.cn("rounded-md border px-2.5 py-2 text-left text-[11px] transition-colors", mode === "keep" ? "border-primary bg-primary/10" : "text-muted-foreground hover:border-primary/50"),
|
|
6127
6203
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6128
6204
|
className: "block font-medium text-foreground",
|
|
6129
6205
|
children: "保持原位置"
|
|
@@ -6185,16 +6261,23 @@ function useAddToTimeline() {
|
|
|
6185
6261
|
const assets = (0, _cut_workbench_react.useEditor)((s) => s.assets);
|
|
6186
6262
|
const playheadUs = (0, _cut_workbench_react.useEditor)((s) => s.playheadUs);
|
|
6187
6263
|
const addClip = (0, _cut_workbench_react.useEditor)((s) => s.addClip);
|
|
6264
|
+
const { bridge } = require_notify.useWorkbench();
|
|
6188
6265
|
return (0, react.useCallback)((assetId) => {
|
|
6189
6266
|
const asset = assets.find((a) => a.id === assetId);
|
|
6190
6267
|
if (!asset) {
|
|
6191
|
-
|
|
6268
|
+
require_notify.notify(bridge, {
|
|
6269
|
+
level: "error",
|
|
6270
|
+
title: "素材不存在,可能已被删除"
|
|
6271
|
+
});
|
|
6192
6272
|
return;
|
|
6193
6273
|
}
|
|
6194
6274
|
const kindIsAudio = asset.kind === "audio";
|
|
6195
6275
|
const track = project.tracks.find((t) => t.kind === (kindIsAudio ? "audio" : "visual")) ?? project.tracks[0];
|
|
6196
6276
|
if (!track) {
|
|
6197
|
-
|
|
6277
|
+
require_notify.notify(bridge, {
|
|
6278
|
+
level: "error",
|
|
6279
|
+
title: "没有可用轨道"
|
|
6280
|
+
});
|
|
6198
6281
|
return;
|
|
6199
6282
|
}
|
|
6200
6283
|
const trackEnd = (0, _cut_workbench_core.getTrackEnd)(project, track.id);
|
|
@@ -6227,6 +6310,7 @@ function useAddToTimeline() {
|
|
|
6227
6310
|
}, [
|
|
6228
6311
|
addClip,
|
|
6229
6312
|
assets,
|
|
6313
|
+
bridge,
|
|
6230
6314
|
playheadUs,
|
|
6231
6315
|
project
|
|
6232
6316
|
]);
|
|
@@ -6280,7 +6364,7 @@ var FRAME_US = 1e6 / 30;
|
|
|
6280
6364
|
*/
|
|
6281
6365
|
function useKeyboardShortcuts() {
|
|
6282
6366
|
const editor = (0, _cut_workbench_react.useCutEditor)();
|
|
6283
|
-
const uiApi = useUiStoreApi();
|
|
6367
|
+
const uiApi = require_notify.useUiStoreApi();
|
|
6284
6368
|
const handler = (0, react.useCallback)((event) => {
|
|
6285
6369
|
const target = event.target;
|
|
6286
6370
|
const isEditable = target != null && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable || target.getAttribute("role") === "textbox" || target.closest("[role=menu]") != null);
|
|
@@ -6460,17 +6544,17 @@ var LabPage = (0, react.lazy)(() => import("@cut-workbench/workbench/lab").then(
|
|
|
6460
6544
|
function CutWorkbench({ slots, toolbar: toolbarOptions, className, onSaveProject, onOpenProject, enforceCapability = true }) {
|
|
6461
6545
|
const editor = (0, _cut_workbench_react.useCutEditor)();
|
|
6462
6546
|
const capability = useCapabilityInfo();
|
|
6463
|
-
const leftWidth = useUi((s) => s.leftWidth);
|
|
6464
|
-
const rightWidth = useUi((s) => s.rightWidth);
|
|
6465
|
-
const timelineHeight = useUi((s) => s.timelineHeight);
|
|
6466
|
-
const setLeftWidth = useUi((s) => s.setLeftWidth);
|
|
6467
|
-
const setRightWidth = useUi((s) => s.setRightWidth);
|
|
6468
|
-
const setTimelineHeight = useUi((s) => s.setTimelineHeight);
|
|
6469
|
-
const openDialog = useUi((s) => s.openDialog);
|
|
6470
|
-
const closeDialog = useUi((s) => s.closeDialog);
|
|
6471
|
-
const dialog = useUi((s) => s.dialog);
|
|
6472
|
-
const { config, theme } = useWorkbench();
|
|
6473
|
-
const route = useUi((s) => s.route);
|
|
6547
|
+
const leftWidth = require_notify.useUi((s) => s.leftWidth);
|
|
6548
|
+
const rightWidth = require_notify.useUi((s) => s.rightWidth);
|
|
6549
|
+
const timelineHeight = require_notify.useUi((s) => s.timelineHeight);
|
|
6550
|
+
const setLeftWidth = require_notify.useUi((s) => s.setLeftWidth);
|
|
6551
|
+
const setRightWidth = require_notify.useUi((s) => s.setRightWidth);
|
|
6552
|
+
const setTimelineHeight = require_notify.useUi((s) => s.setTimelineHeight);
|
|
6553
|
+
const openDialog = require_notify.useUi((s) => s.openDialog);
|
|
6554
|
+
const closeDialog = require_notify.useUi((s) => s.closeDialog);
|
|
6555
|
+
const dialog = require_notify.useUi((s) => s.dialog);
|
|
6556
|
+
const { config, theme } = require_notify.useWorkbench();
|
|
6557
|
+
const route = require_notify.useUi((s) => s.route);
|
|
6474
6558
|
const addToTimeline = useAddToTimeline();
|
|
6475
6559
|
const removeAsset = (0, _cut_workbench_react.useEditor)((s) => s.removeAsset);
|
|
6476
6560
|
const [busy, setBusy] = (0, react.useState)(null);
|
|
@@ -7517,11 +7601,15 @@ function CutWorkbenchProvider({ children, bridge, theme = "dark", tokens, config
|
|
|
7517
7601
|
(0, _cut_workbench_react.useCutEditor)();
|
|
7518
7602
|
const [ui] = (0, react.useState)(() => createUiStore());
|
|
7519
7603
|
const resolvedBridge = (0, react.useMemo)(() => bridge ?? createDefaultBridge(), [bridge]);
|
|
7604
|
+
(0, react.useEffect)(() => {
|
|
7605
|
+
require_notify.setDefaultBridge(resolvedBridge);
|
|
7606
|
+
return () => require_notify.setDefaultBridge(null);
|
|
7607
|
+
}, [resolvedBridge]);
|
|
7520
7608
|
const mergedConfig = (0, react.useMemo)(() => ({
|
|
7521
|
-
...DEFAULT_CONFIG,
|
|
7609
|
+
...require_notify.DEFAULT_CONFIG,
|
|
7522
7610
|
...config,
|
|
7523
7611
|
features: {
|
|
7524
|
-
...DEFAULT_CONFIG.features,
|
|
7612
|
+
...require_notify.DEFAULT_CONFIG.features,
|
|
7525
7613
|
...config?.features ?? {}
|
|
7526
7614
|
}
|
|
7527
7615
|
}), [config]);
|
|
@@ -7550,7 +7638,7 @@ function CutWorkbenchProvider({ children, bridge, theme = "dark", tokens, config
|
|
|
7550
7638
|
resolvedTheme,
|
|
7551
7639
|
portalContainer
|
|
7552
7640
|
]);
|
|
7553
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkbenchContext.Provider, {
|
|
7641
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_notify.WorkbenchContext.Provider, {
|
|
7554
7642
|
value,
|
|
7555
7643
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PortalContainerProvider, {
|
|
7556
7644
|
container: portalContainer,
|
|
@@ -7803,8 +7891,8 @@ exports.ClipBlock = ClipBlock;
|
|
|
7803
7891
|
exports.ClipContextMenu = ClipContextMenu;
|
|
7804
7892
|
exports.CutWorkbench = CutWorkbench;
|
|
7805
7893
|
exports.CutWorkbenchProvider = CutWorkbenchProvider;
|
|
7806
|
-
exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
|
|
7807
|
-
exports.DEFAULT_FEATURES = DEFAULT_FEATURES;
|
|
7894
|
+
exports.DEFAULT_CONFIG = require_notify.DEFAULT_CONFIG;
|
|
7895
|
+
exports.DEFAULT_FEATURES = require_notify.DEFAULT_FEATURES;
|
|
7808
7896
|
exports.DropZone = DropZone;
|
|
7809
7897
|
exports.EmptyState = EmptyState;
|
|
7810
7898
|
exports.ExportDialog = ExportDialog;
|
|
@@ -7852,8 +7940,8 @@ exports.useExport = useExport;
|
|
|
7852
7940
|
exports.useHostDarkClass = useHostDarkClass;
|
|
7853
7941
|
exports.useMediaImport = useMediaImport;
|
|
7854
7942
|
exports.usePortalContainer = usePortalContainer;
|
|
7855
|
-
exports.useUi = useUi;
|
|
7856
|
-
exports.useUiStoreApi = useUiStoreApi;
|
|
7857
|
-
exports.useWorkbench = useWorkbench;
|
|
7943
|
+
exports.useUi = require_notify.useUi;
|
|
7944
|
+
exports.useUiStoreApi = require_notify.useUiStoreApi;
|
|
7945
|
+
exports.useWorkbench = require_notify.useWorkbench;
|
|
7858
7946
|
|
|
7859
7947
|
//# sourceMappingURL=index.cjs.map
|