@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.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { _ as useUi, a as formatElapsed, b as Badge, c as formatSeconds, f as CardHeader, g as WorkbenchContext, h as DEFAULT_FEATURES, i as formatDuration, l as Card, m as DEFAULT_CONFIG, n as setDefaultBridge, o as formatPercent, p as CardTitle, r as formatBytes, s as formatResolution, t as notify, u as CardContent, v as useUiStoreApi, x as cn, y as useWorkbench } from "./notify-CEwFQgjC.js";
|
|
2
2
|
import { createRoot } from "react-dom/client";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Suspense, createContext, createElement, lazy, memo, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
@@ -9,7 +9,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
9
9
|
import { AlertOctagon, AlertTriangle, Camera, CheckCircle2, CheckIcon, ChevronDownIcon, ChevronUpIcon, CircleCheckIcon, Copy, Download, Eye, EyeOff, Film, FlaskConical, FlipHorizontal2, FlipVertical2, FolderOpen, Image, Info, InfoIcon, Layers, Link2, Link2Off, Loader2, Loader2Icon, Lock, LockOpen, Magnet, Maximize2, Monitor, Music, OctagonXIcon, Pause, Play, Plus, Redo2, RotateCcw, RotateCw, Save, Scissors, Settings2, SkipBack, SkipForward, StepBack, StepForward, Subtitles, Trash2, TriangleAlertIcon, Type, Undo2, Upload, Video, Volume2, VolumeX, Wrench, X, XCircle, XIcon, ZoomIn, ZoomOut } from "lucide-react";
|
|
10
10
|
import { cva } from "class-variance-authority";
|
|
11
11
|
import { ContextMenu, Dialog, DropdownMenu, Label, Popover, Progress, ScrollArea, Select, Separator, Slider, Slot, Switch, Tabs, Tooltip } from "radix-ui";
|
|
12
|
-
import { Toaster
|
|
12
|
+
import { Toaster } from "sonner";
|
|
13
13
|
import { useDropzone } from "react-dropzone";
|
|
14
14
|
import { createStore } from "zustand/vanilla";
|
|
15
15
|
// src/components/layout/AppShell.tsx
|
|
@@ -305,44 +305,6 @@ function Row$1({ label, ok, optional }) {
|
|
|
305
305
|
})]
|
|
306
306
|
});
|
|
307
307
|
}
|
|
308
|
-
// src/context.tsx
|
|
309
|
-
/**
|
|
310
|
-
* 工作台自身的 Context:UI 状态(面板尺寸 / 缩放 / 对话框)+ 宿主桥。
|
|
311
|
-
*
|
|
312
|
-
* 与 `@cut-workbench/react` 的编辑器 Context 分开:编辑器是「数据与引擎」,
|
|
313
|
-
* 这里是「某一次挂载的布局与交互」。两者生命周期可以不同(例如宿主切换项目时
|
|
314
|
-
* 复用同一个编辑器、只重建 UI)。
|
|
315
|
-
*/
|
|
316
|
-
var WorkbenchContext = createContext(null);
|
|
317
|
-
function useWorkbench() {
|
|
318
|
-
const value = useContext(WorkbenchContext);
|
|
319
|
-
if (value == null) throw new Error("[cut-workbench] 缺少 <WorkbenchProvider>:组件必须挂在工作台内部");
|
|
320
|
-
return value;
|
|
321
|
-
}
|
|
322
|
-
/** 订阅 UI 状态(带 selector) */
|
|
323
|
-
function useUi(selector) {
|
|
324
|
-
const { ui } = useWorkbench();
|
|
325
|
-
return useSyncExternalStore(ui.subscribe, () => selector(ui.getState()), () => selector(ui.getState()));
|
|
326
|
-
}
|
|
327
|
-
/** 取 UI store 本体(事件回调里读写用) */
|
|
328
|
-
function useUiStoreApi() {
|
|
329
|
-
return useWorkbench().ui;
|
|
330
|
-
}
|
|
331
|
-
/** 默认功能开关 */
|
|
332
|
-
var DEFAULT_FEATURES = {
|
|
333
|
-
mediaLibrary: true,
|
|
334
|
-
preview: true,
|
|
335
|
-
timeline: true,
|
|
336
|
-
inspector: true,
|
|
337
|
-
export: true,
|
|
338
|
-
canvasSettings: true,
|
|
339
|
-
lab: false
|
|
340
|
-
};
|
|
341
|
-
var DEFAULT_CONFIG = {
|
|
342
|
-
features: DEFAULT_FEATURES,
|
|
343
|
-
keyboard: "global",
|
|
344
|
-
resizable: true
|
|
345
|
-
};
|
|
346
308
|
// src/components/layout/WorkbenchToolbar.tsx
|
|
347
309
|
/**
|
|
348
310
|
* 内置顶栏(WorkbenchToolbar)。
|
|
@@ -1019,8 +981,164 @@ function EmptyState({ icon: Icon, title, description, action, className, compact
|
|
|
1019
981
|
]
|
|
1020
982
|
});
|
|
1021
983
|
}
|
|
984
|
+
// src/features/media-library/lib/pick-files.ts
|
|
985
|
+
var INPUT_ID = "cut-workbench-file-input";
|
|
986
|
+
/** 内置回退:隐藏 input,选完即弃 */
|
|
987
|
+
function pickWithInput(accept, multiple) {
|
|
988
|
+
return new Promise((resolve) => {
|
|
989
|
+
const input = document.createElement("input");
|
|
990
|
+
input.type = "file";
|
|
991
|
+
input.id = INPUT_ID;
|
|
992
|
+
input.multiple = multiple;
|
|
993
|
+
if (accept.length > 0) input.accept = accept.join(",");
|
|
994
|
+
input.style.display = "none";
|
|
995
|
+
document.body.appendChild(input);
|
|
996
|
+
const finish = (files) => {
|
|
997
|
+
input.remove();
|
|
998
|
+
resolve(files);
|
|
999
|
+
};
|
|
1000
|
+
input.addEventListener("change", () => finish(Array.from(input.files ?? [])), { once: true });
|
|
1001
|
+
window.addEventListener("focus", () => {
|
|
1002
|
+
window.setTimeout(() => {
|
|
1003
|
+
if (document.body.contains(input)) finish([]);
|
|
1004
|
+
}, 300);
|
|
1005
|
+
}, { once: true });
|
|
1006
|
+
input.click();
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* @returns 选中的文件;用户取消时为空数组
|
|
1011
|
+
*/
|
|
1012
|
+
async function pickMediaFiles(bridge, options) {
|
|
1013
|
+
const multiple = options.multiple ?? true;
|
|
1014
|
+
if (bridge?.files?.pickFiles != null) {
|
|
1015
|
+
const result = await bridge.files.pickFiles({
|
|
1016
|
+
accept: options.accept,
|
|
1017
|
+
multiple,
|
|
1018
|
+
description: options.description
|
|
1019
|
+
});
|
|
1020
|
+
if (result != null) return result;
|
|
1021
|
+
}
|
|
1022
|
+
return pickWithInput(options.accept, multiple);
|
|
1023
|
+
}
|
|
1024
|
+
// src/features/media-library/hooks/useMediaImport.ts
|
|
1025
|
+
var ACCEPT = [
|
|
1026
|
+
"video/mp4",
|
|
1027
|
+
"audio/*",
|
|
1028
|
+
"image/*",
|
|
1029
|
+
".srt",
|
|
1030
|
+
"text/plain"
|
|
1031
|
+
];
|
|
1032
|
+
/**
|
|
1033
|
+
* 找出可以「重连」的素材。
|
|
1034
|
+
*
|
|
1035
|
+
* 场景:刷新后工程从 IndexedDB 恢复,但素材二进制消失(File 无法持久化)。
|
|
1036
|
+
* 用户重新导入同一个文件时,复用原 assetId,时间轴上已有片段立即恢复。
|
|
1037
|
+
* 匹配条件:同名 + 同类型 + 当前确实缺少二进制。
|
|
1038
|
+
*/
|
|
1039
|
+
function findReconnectableAssetId(state, file, registry) {
|
|
1040
|
+
if (state.missingAssetIds.length === 0) return null;
|
|
1041
|
+
return state.assets.find((asset) => state.missingAssetIds.includes(asset.id) && asset.name === file.name && registry.getFile(asset.id) == null)?.id ?? null;
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* 批量导入素材。
|
|
1045
|
+
*
|
|
1046
|
+
* - 串行处理,避免同时开辟多个解码器把内存打爆;
|
|
1047
|
+
* - 单个文件失败只影响自身,最后汇总 toast;
|
|
1048
|
+
* - 进度写在本地 state(不放进 store,避免高频更新引起全局重渲染);
|
|
1049
|
+
* - 若与「缺失源文件」的素材同名同类型,则重连而不是新增。
|
|
1050
|
+
*/
|
|
1051
|
+
function useMediaImport() {
|
|
1052
|
+
const editor = useCutEditor();
|
|
1053
|
+
const clips = editor.runtime.clips;
|
|
1054
|
+
const store = editor.store;
|
|
1055
|
+
const [progress, setProgress] = useState([]);
|
|
1056
|
+
const [importing, setImporting] = useState(false);
|
|
1057
|
+
const addAsset = useEditor((s) => s.addAsset);
|
|
1058
|
+
const replaceAsset = useEditor((s) => s.replaceAsset);
|
|
1059
|
+
const clearMissing = useEditor((s) => s.clearMissing);
|
|
1060
|
+
const selectAsset = useEditor((s) => s.selectAsset);
|
|
1061
|
+
const beginImport = useUi((s) => s.beginImport);
|
|
1062
|
+
const endImport = useUi((s) => s.endImport);
|
|
1063
|
+
const { bridge } = useWorkbench();
|
|
1064
|
+
return {
|
|
1065
|
+
importing,
|
|
1066
|
+
progress,
|
|
1067
|
+
importFiles: useCallback(async (files) => {
|
|
1068
|
+
if (files.length === 0) return [];
|
|
1069
|
+
setImporting(true);
|
|
1070
|
+
beginImport();
|
|
1071
|
+
setProgress(files.map((f) => ({
|
|
1072
|
+
name: f.name,
|
|
1073
|
+
progress: 0
|
|
1074
|
+
})));
|
|
1075
|
+
const imported = [];
|
|
1076
|
+
const failures = [];
|
|
1077
|
+
let reconnected = 0;
|
|
1078
|
+
for (let i = 0; i < files.length; i += 1) {
|
|
1079
|
+
const file = files[i];
|
|
1080
|
+
if (!file) continue;
|
|
1081
|
+
try {
|
|
1082
|
+
const reconnectId = findReconnectableAssetId(store.getState(), file, clips);
|
|
1083
|
+
const result = await importFile(clips, file, (p) => {
|
|
1084
|
+
setProgress((prev) => prev.map((item, index) => index === i ? {
|
|
1085
|
+
...item,
|
|
1086
|
+
progress: p
|
|
1087
|
+
} : item));
|
|
1088
|
+
});
|
|
1089
|
+
if (reconnectId != null) {
|
|
1090
|
+
clips.replaceAssetSource(reconnectId, file, result.master ?? null);
|
|
1091
|
+
replaceAsset(reconnectId, result.asset);
|
|
1092
|
+
clearMissing(reconnectId);
|
|
1093
|
+
imported.push({
|
|
1094
|
+
...result.asset,
|
|
1095
|
+
id: reconnectId
|
|
1096
|
+
});
|
|
1097
|
+
reconnected += 1;
|
|
1098
|
+
} else {
|
|
1099
|
+
addAsset(result.asset);
|
|
1100
|
+
imported.push(result.asset);
|
|
1101
|
+
}
|
|
1102
|
+
} catch (err) {
|
|
1103
|
+
const message = err instanceof UnsupportedFileError ? err.message : err instanceof Error ? err.message : String(err);
|
|
1104
|
+
failures.push(`${file.name}:${message}`);
|
|
1105
|
+
setProgress((prev) => prev.map((item, index) => index === i ? {
|
|
1106
|
+
...item,
|
|
1107
|
+
error: message
|
|
1108
|
+
} : item));
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
setImporting(false);
|
|
1112
|
+
endImport();
|
|
1113
|
+
if (imported.length > 0) {
|
|
1114
|
+
selectAsset(imported[0]?.id ?? null);
|
|
1115
|
+
notify(bridge, {
|
|
1116
|
+
level: "success",
|
|
1117
|
+
title: reconnected > 0 ? `已导入 ${imported.length} 个素材(其中 ${reconnected} 个重连到已有片段)` : `已导入 ${imported.length} 个素材`
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
if (failures.length > 0) notify(bridge, {
|
|
1121
|
+
level: "error",
|
|
1122
|
+
title: `${failures.length} 个文件导入失败`,
|
|
1123
|
+
description: failures.slice(0, 3).join("\n"),
|
|
1124
|
+
duration: 8e3
|
|
1125
|
+
});
|
|
1126
|
+
window.setTimeout(() => setProgress([]), 3e3);
|
|
1127
|
+
return imported;
|
|
1128
|
+
}, [
|
|
1129
|
+
addAsset,
|
|
1130
|
+
beginImport,
|
|
1131
|
+
bridge,
|
|
1132
|
+
clearMissing,
|
|
1133
|
+
endImport,
|
|
1134
|
+
replaceAsset,
|
|
1135
|
+
selectAsset
|
|
1136
|
+
])
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1022
1139
|
// src/features/media-library/components/DropZone.tsx
|
|
1023
|
-
|
|
1140
|
+
/** react-dropzone 用的 MIME → 扩展名映射 */
|
|
1141
|
+
var DROPZONE_ACCEPT = {
|
|
1024
1142
|
"video/mp4": [".mp4", ".m4v"],
|
|
1025
1143
|
"audio/*": [
|
|
1026
1144
|
".mp3",
|
|
@@ -1043,22 +1161,45 @@ var ACCEPT = {
|
|
|
1043
1161
|
/**
|
|
1044
1162
|
* 拖拽 / 点击导入区域。
|
|
1045
1163
|
*
|
|
1164
|
+
* 两条入口:
|
|
1165
|
+
* - **拖拽**:走 react-dropzone(宿主无法介入,这是浏览器原生行为);
|
|
1166
|
+
* - **点击**:优先走 `HostBridge.files.pickFiles` —— 宿主可能不用系统文件
|
|
1167
|
+
* 对话框,而是从自己的素材库 / 云端 / URL 导入。宿主返回 `null`(不接管)
|
|
1168
|
+
* 时才回退到内置 `<input type=file>`。
|
|
1169
|
+
*
|
|
1046
1170
|
* 注意:react-dropzone 的 accept 只做初筛,真正按 MIME + 扩展名校验在
|
|
1047
1171
|
* core/media/file-reader,从而保证「拖入 .webm 得到明确错误」而不是被静默拒绝。
|
|
1048
1172
|
*/
|
|
1049
1173
|
function DropZone({ onFiles, disabled, className, compact }) {
|
|
1050
|
-
const {
|
|
1174
|
+
const { bridge } = useWorkbench();
|
|
1175
|
+
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
|
1051
1176
|
onDrop: (accepted) => {
|
|
1052
1177
|
if (accepted.length > 0) onFiles(accepted);
|
|
1053
1178
|
},
|
|
1054
|
-
noClick:
|
|
1055
|
-
noKeyboard:
|
|
1179
|
+
noClick: true,
|
|
1180
|
+
noKeyboard: true,
|
|
1056
1181
|
multiple: true,
|
|
1057
1182
|
disabled,
|
|
1058
|
-
accept:
|
|
1059
|
-
});
|
|
1183
|
+
accept: DROPZONE_ACCEPT
|
|
1184
|
+
});
|
|
1185
|
+
const handleClick = useCallback(() => {
|
|
1186
|
+
if (disabled) return;
|
|
1187
|
+
pickMediaFiles(bridge, {
|
|
1188
|
+
accept: ACCEPT,
|
|
1189
|
+
multiple: true,
|
|
1190
|
+
description: "选择要导入的素材"
|
|
1191
|
+
}).then((files) => {
|
|
1192
|
+
if (files.length > 0) onFiles(files);
|
|
1193
|
+
});
|
|
1194
|
+
}, [
|
|
1195
|
+
bridge,
|
|
1196
|
+
disabled,
|
|
1197
|
+
onFiles
|
|
1198
|
+
]);
|
|
1060
1199
|
return /* @__PURE__ */ jsxs("div", {
|
|
1061
1200
|
...getRootProps(),
|
|
1201
|
+
onClick: handleClick,
|
|
1202
|
+
"data-drop-zone": "",
|
|
1062
1203
|
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
1204
|
children: [
|
|
1064
1205
|
/* @__PURE__ */ jsx("input", { ...getInputProps() }),
|
|
@@ -1075,7 +1216,7 @@ function DropZone({ onFiles, disabled, className, compact }) {
|
|
|
1075
1216
|
className: "sr-only",
|
|
1076
1217
|
onClick: (e) => {
|
|
1077
1218
|
e.stopPropagation();
|
|
1078
|
-
|
|
1219
|
+
handleClick();
|
|
1079
1220
|
},
|
|
1080
1221
|
children: "选择文件"
|
|
1081
1222
|
})
|
|
@@ -1296,107 +1437,6 @@ function MediaGrid({ assets, selectedAssetId, missingAssetIds = [], progress, on
|
|
|
1296
1437
|
]
|
|
1297
1438
|
});
|
|
1298
1439
|
}
|
|
1299
|
-
// src/features/media-library/hooks/useMediaImport.ts
|
|
1300
|
-
/**
|
|
1301
|
-
* 找出可以「重连」的素材。
|
|
1302
|
-
*
|
|
1303
|
-
* 场景:刷新后工程从 IndexedDB 恢复,但素材二进制消失(File 无法持久化)。
|
|
1304
|
-
* 用户重新导入同一个文件时,复用原 assetId,时间轴上已有片段立即恢复。
|
|
1305
|
-
* 匹配条件:同名 + 同类型 + 当前确实缺少二进制。
|
|
1306
|
-
*/
|
|
1307
|
-
function findReconnectableAssetId(state, file, registry) {
|
|
1308
|
-
if (state.missingAssetIds.length === 0) return null;
|
|
1309
|
-
return state.assets.find((asset) => state.missingAssetIds.includes(asset.id) && asset.name === file.name && registry.getFile(asset.id) == null)?.id ?? null;
|
|
1310
|
-
}
|
|
1311
|
-
/**
|
|
1312
|
-
* 批量导入素材。
|
|
1313
|
-
*
|
|
1314
|
-
* - 串行处理,避免同时开辟多个解码器把内存打爆;
|
|
1315
|
-
* - 单个文件失败只影响自身,最后汇总 toast;
|
|
1316
|
-
* - 进度写在本地 state(不放进 store,避免高频更新引起全局重渲染);
|
|
1317
|
-
* - 若与「缺失源文件」的素材同名同类型,则重连而不是新增。
|
|
1318
|
-
*/
|
|
1319
|
-
function useMediaImport() {
|
|
1320
|
-
const editor = useCutEditor();
|
|
1321
|
-
const clips = editor.runtime.clips;
|
|
1322
|
-
const store = editor.store;
|
|
1323
|
-
const [progress, setProgress] = useState([]);
|
|
1324
|
-
const [importing, setImporting] = useState(false);
|
|
1325
|
-
const addAsset = useEditor((s) => s.addAsset);
|
|
1326
|
-
const replaceAsset = useEditor((s) => s.replaceAsset);
|
|
1327
|
-
const clearMissing = useEditor((s) => s.clearMissing);
|
|
1328
|
-
const selectAsset = useEditor((s) => s.selectAsset);
|
|
1329
|
-
const beginImport = useUi((s) => s.beginImport);
|
|
1330
|
-
const endImport = useUi((s) => s.endImport);
|
|
1331
|
-
return {
|
|
1332
|
-
importing,
|
|
1333
|
-
progress,
|
|
1334
|
-
importFiles: useCallback(async (files) => {
|
|
1335
|
-
if (files.length === 0) return [];
|
|
1336
|
-
setImporting(true);
|
|
1337
|
-
beginImport();
|
|
1338
|
-
setProgress(files.map((f) => ({
|
|
1339
|
-
name: f.name,
|
|
1340
|
-
progress: 0
|
|
1341
|
-
})));
|
|
1342
|
-
const imported = [];
|
|
1343
|
-
const failures = [];
|
|
1344
|
-
let reconnected = 0;
|
|
1345
|
-
for (let i = 0; i < files.length; i += 1) {
|
|
1346
|
-
const file = files[i];
|
|
1347
|
-
if (!file) continue;
|
|
1348
|
-
try {
|
|
1349
|
-
const reconnectId = findReconnectableAssetId(store.getState(), file, clips);
|
|
1350
|
-
const result = await importFile(clips, file, (p) => {
|
|
1351
|
-
setProgress((prev) => prev.map((item, index) => index === i ? {
|
|
1352
|
-
...item,
|
|
1353
|
-
progress: p
|
|
1354
|
-
} : item));
|
|
1355
|
-
});
|
|
1356
|
-
if (reconnectId != null) {
|
|
1357
|
-
clips.replaceAssetSource(reconnectId, file, result.master ?? null);
|
|
1358
|
-
replaceAsset(reconnectId, result.asset);
|
|
1359
|
-
clearMissing(reconnectId);
|
|
1360
|
-
imported.push({
|
|
1361
|
-
...result.asset,
|
|
1362
|
-
id: reconnectId
|
|
1363
|
-
});
|
|
1364
|
-
reconnected += 1;
|
|
1365
|
-
} else {
|
|
1366
|
-
addAsset(result.asset);
|
|
1367
|
-
imported.push(result.asset);
|
|
1368
|
-
}
|
|
1369
|
-
} catch (err) {
|
|
1370
|
-
const message = err instanceof UnsupportedFileError ? err.message : err instanceof Error ? err.message : String(err);
|
|
1371
|
-
failures.push(`${file.name}:${message}`);
|
|
1372
|
-
setProgress((prev) => prev.map((item, index) => index === i ? {
|
|
1373
|
-
...item,
|
|
1374
|
-
error: message
|
|
1375
|
-
} : item));
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
setImporting(false);
|
|
1379
|
-
endImport();
|
|
1380
|
-
if (imported.length > 0) {
|
|
1381
|
-
selectAsset(imported[0]?.id ?? null);
|
|
1382
|
-
toast.success(reconnected > 0 ? `已导入 ${imported.length} 个素材(其中 ${reconnected} 个重连到已有片段)` : `已导入 ${imported.length} 个素材`);
|
|
1383
|
-
}
|
|
1384
|
-
if (failures.length > 0) toast.error(`${failures.length} 个文件导入失败`, {
|
|
1385
|
-
description: failures.slice(0, 3).join("\n"),
|
|
1386
|
-
duration: 8e3
|
|
1387
|
-
});
|
|
1388
|
-
window.setTimeout(() => setProgress([]), 3e3);
|
|
1389
|
-
return imported;
|
|
1390
|
-
}, [
|
|
1391
|
-
addAsset,
|
|
1392
|
-
beginImport,
|
|
1393
|
-
clearMissing,
|
|
1394
|
-
endImport,
|
|
1395
|
-
replaceAsset,
|
|
1396
|
-
selectAsset
|
|
1397
|
-
])
|
|
1398
|
-
};
|
|
1399
|
-
}
|
|
1400
1440
|
// src/features/media-library/components/MediaLibraryPanel.tsx
|
|
1401
1441
|
var KIND_LABEL$1 = {
|
|
1402
1442
|
video: "视频",
|
|
@@ -1564,6 +1604,7 @@ function CreateTextDialog({ open, onOpenChange, onCreated }) {
|
|
|
1564
1604
|
const projectWidth = useEditor((s) => s.project.width);
|
|
1565
1605
|
const projectHeight = useEditor((s) => s.project.height);
|
|
1566
1606
|
const addAsset = useEditor((s) => s.addAsset);
|
|
1607
|
+
const { bridge } = useWorkbench();
|
|
1567
1608
|
const handleCreate = () => {
|
|
1568
1609
|
const { asset } = createTextAsset({
|
|
1569
1610
|
registry: editor.runtime.clips,
|
|
@@ -1574,7 +1615,10 @@ function CreateTextDialog({ open, onOpenChange, onCreated }) {
|
|
|
1574
1615
|
addAsset(asset);
|
|
1575
1616
|
onCreated(asset.id);
|
|
1576
1617
|
onOpenChange(false);
|
|
1577
|
-
|
|
1618
|
+
notify(bridge, {
|
|
1619
|
+
level: "success",
|
|
1620
|
+
title: "已创建文字素材"
|
|
1621
|
+
});
|
|
1578
1622
|
};
|
|
1579
1623
|
return /* @__PURE__ */ jsx(Dialog$1, {
|
|
1580
1624
|
open,
|
|
@@ -1608,10 +1652,15 @@ function CreateSubtitleDialog({ open, onOpenChange, onCreated }) {
|
|
|
1608
1652
|
const projectWidth = useEditor((s) => s.project.width);
|
|
1609
1653
|
const projectHeight = useEditor((s) => s.project.height);
|
|
1610
1654
|
const addAsset = useEditor((s) => s.addAsset);
|
|
1655
|
+
const { bridge } = useWorkbench();
|
|
1611
1656
|
const handleCreate = () => {
|
|
1612
1657
|
const durationUs = parseSrtDurationUs(srt);
|
|
1613
1658
|
if (durationUs <= 0) {
|
|
1614
|
-
|
|
1659
|
+
notify(bridge, {
|
|
1660
|
+
level: "error",
|
|
1661
|
+
title: "SRT 解析失败",
|
|
1662
|
+
description: "请检查时间轴格式:00:00:00,000 --> 00:00:02,000"
|
|
1663
|
+
});
|
|
1615
1664
|
return;
|
|
1616
1665
|
}
|
|
1617
1666
|
const { asset } = createSubtitleAsset({
|
|
@@ -1624,7 +1673,10 @@ function CreateSubtitleDialog({ open, onOpenChange, onCreated }) {
|
|
|
1624
1673
|
addAsset(asset);
|
|
1625
1674
|
onCreated(asset.id);
|
|
1626
1675
|
onOpenChange(false);
|
|
1627
|
-
|
|
1676
|
+
notify(bridge, {
|
|
1677
|
+
level: "success",
|
|
1678
|
+
title: "已创建字幕素材"
|
|
1679
|
+
});
|
|
1628
1680
|
};
|
|
1629
1681
|
return /* @__PURE__ */ jsx(Dialog$1, {
|
|
1630
1682
|
open,
|
|
@@ -1650,11 +1702,22 @@ function CreateSubtitleDialog({ open, onOpenChange, onCreated }) {
|
|
|
1650
1702
|
});
|
|
1651
1703
|
}
|
|
1652
1704
|
// src/hooks/use-toast-safe.ts
|
|
1653
|
-
/**
|
|
1705
|
+
/**
|
|
1706
|
+
* 把异常转成可见提示,禁止静默吞掉。
|
|
1707
|
+
*
|
|
1708
|
+
* 这是**非 hook** 版本,供事件回调 / 非组件代码调用(例如 `useAvCanvas` 的
|
|
1709
|
+
* 初始化失败)。它拿不到 React context,因此走 `notify` 注册的默认桥:
|
|
1710
|
+
* `WorkbenchProvider` 挂载时会把 `bridge` 注册进来,宿主实现了 `notify`
|
|
1711
|
+
* 就交给宿主,否则退回库内 toast。
|
|
1712
|
+
*/
|
|
1654
1713
|
function reportError(error, fallback = "操作失败") {
|
|
1655
1714
|
const message = error instanceof Error ? error.message : String(error);
|
|
1656
1715
|
console.error(error);
|
|
1657
|
-
|
|
1716
|
+
notify(void 0, {
|
|
1717
|
+
level: "error",
|
|
1718
|
+
title: fallback,
|
|
1719
|
+
description: message
|
|
1720
|
+
});
|
|
1658
1721
|
}
|
|
1659
1722
|
// src/features/preview/hooks/useAvCanvas.ts
|
|
1660
1723
|
/**
|
|
@@ -2574,6 +2637,7 @@ function PreviewPanel() {
|
|
|
2574
2637
|
const setMasterVolume = useEditor((s) => s.setMasterVolume);
|
|
2575
2638
|
const playheadUs = useEditor((s) => s.playheadUs);
|
|
2576
2639
|
const selectedClipIds = useEditor((s) => s.selectedClipIds);
|
|
2640
|
+
const { bridge } = useWorkbench();
|
|
2577
2641
|
const canvas = useAvCanvas({
|
|
2578
2642
|
width: project.width,
|
|
2579
2643
|
height: project.height
|
|
@@ -2584,12 +2648,18 @@ function PreviewPanel() {
|
|
|
2584
2648
|
if (errors.length === 0) return;
|
|
2585
2649
|
const missing = errors.filter((e) => e.message.startsWith("素材文件未载入"));
|
|
2586
2650
|
const other = errors.filter((e) => !e.message.startsWith("素材文件未载入"));
|
|
2587
|
-
if (missing.length > 0)
|
|
2651
|
+
if (missing.length > 0) notify(bridge, {
|
|
2652
|
+
level: "warning",
|
|
2653
|
+
title: `${missing.length} 个片段缺少源文件,已跳过预览`,
|
|
2588
2654
|
description: "刷新后素材二进制不会保留,请重新导入相同文件",
|
|
2589
2655
|
duration: 8e3
|
|
2590
2656
|
});
|
|
2591
|
-
if (other.length > 0)
|
|
2592
|
-
|
|
2657
|
+
if (other.length > 0) notify(bridge, {
|
|
2658
|
+
level: "error",
|
|
2659
|
+
title: "部分片段无法载入预览",
|
|
2660
|
+
description: Array.from(new Set(other.map((e) => e.message))).slice(0, 3).join("\n")
|
|
2661
|
+
});
|
|
2662
|
+
}, [bridge]);
|
|
2593
2663
|
const clock = usePlaybackClock({
|
|
2594
2664
|
project,
|
|
2595
2665
|
assets,
|
|
@@ -2725,12 +2795,18 @@ function PreviewPanel() {
|
|
|
2725
2795
|
const handleCapture = useCallback(() => {
|
|
2726
2796
|
const dataUrl = canvas.captureImage();
|
|
2727
2797
|
if (dataUrl == null) {
|
|
2728
|
-
|
|
2798
|
+
notify(bridge, {
|
|
2799
|
+
level: "error",
|
|
2800
|
+
title: "截图失败:画布尚未就绪"
|
|
2801
|
+
});
|
|
2729
2802
|
return;
|
|
2730
2803
|
}
|
|
2731
2804
|
downloadDataUrl(dataUrl, `frame-${Date.now()}.png`);
|
|
2732
|
-
|
|
2733
|
-
|
|
2805
|
+
notify(bridge, {
|
|
2806
|
+
level: "success",
|
|
2807
|
+
title: "已导出当前帧"
|
|
2808
|
+
});
|
|
2809
|
+
}, [bridge, canvas]);
|
|
2734
2810
|
const handleFullscreen = () => {
|
|
2735
2811
|
const el = canvas.containerRef.current?.parentElement;
|
|
2736
2812
|
if (el == null) return;
|
|
@@ -6184,16 +6260,23 @@ function useAddToTimeline() {
|
|
|
6184
6260
|
const assets = useEditor((s) => s.assets);
|
|
6185
6261
|
const playheadUs = useEditor((s) => s.playheadUs);
|
|
6186
6262
|
const addClip = useEditor((s) => s.addClip);
|
|
6263
|
+
const { bridge } = useWorkbench();
|
|
6187
6264
|
return useCallback((assetId) => {
|
|
6188
6265
|
const asset = assets.find((a) => a.id === assetId);
|
|
6189
6266
|
if (!asset) {
|
|
6190
|
-
|
|
6267
|
+
notify(bridge, {
|
|
6268
|
+
level: "error",
|
|
6269
|
+
title: "素材不存在,可能已被删除"
|
|
6270
|
+
});
|
|
6191
6271
|
return;
|
|
6192
6272
|
}
|
|
6193
6273
|
const kindIsAudio = asset.kind === "audio";
|
|
6194
6274
|
const track = project.tracks.find((t) => t.kind === (kindIsAudio ? "audio" : "visual")) ?? project.tracks[0];
|
|
6195
6275
|
if (!track) {
|
|
6196
|
-
|
|
6276
|
+
notify(bridge, {
|
|
6277
|
+
level: "error",
|
|
6278
|
+
title: "没有可用轨道"
|
|
6279
|
+
});
|
|
6197
6280
|
return;
|
|
6198
6281
|
}
|
|
6199
6282
|
const trackEnd = getTrackEnd(project, track.id);
|
|
@@ -6226,6 +6309,7 @@ function useAddToTimeline() {
|
|
|
6226
6309
|
}, [
|
|
6227
6310
|
addClip,
|
|
6228
6311
|
assets,
|
|
6312
|
+
bridge,
|
|
6229
6313
|
playheadUs,
|
|
6230
6314
|
project
|
|
6231
6315
|
]);
|
|
@@ -7516,6 +7600,10 @@ function CutWorkbenchProvider({ children, bridge, theme = "dark", tokens, config
|
|
|
7516
7600
|
useCutEditor();
|
|
7517
7601
|
const [ui] = useState(() => createUiStore());
|
|
7518
7602
|
const resolvedBridge = useMemo(() => bridge ?? createDefaultBridge(), [bridge]);
|
|
7603
|
+
useEffect(() => {
|
|
7604
|
+
setDefaultBridge(resolvedBridge);
|
|
7605
|
+
return () => setDefaultBridge(null);
|
|
7606
|
+
}, [resolvedBridge]);
|
|
7519
7607
|
const mergedConfig = useMemo(() => ({
|
|
7520
7608
|
...DEFAULT_CONFIG,
|
|
7521
7609
|
...config,
|