@huyooo/file-explorer-bridge-electron 0.2.1 → 0.4.2

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.
@@ -1,5 +1,5 @@
1
1
  // src/main/index.ts
2
- import { ipcMain, shell } from "electron";
2
+ import { ipcMain, shell, BrowserWindow } from "electron";
3
3
  import path from "path";
4
4
  import {
5
5
  readDirectory,
@@ -21,13 +21,25 @@ import {
21
21
  pasteFiles,
22
22
  searchFiles,
23
23
  searchFilesStream,
24
+ compressFiles,
25
+ extractArchive,
26
+ isArchiveFile,
27
+ getWatchManager,
28
+ showFileInfo,
29
+ openInTerminal,
30
+ openInEditor,
24
31
  getFileType,
25
32
  formatFileSize,
26
- formatDateTime
33
+ formatDateTime,
34
+ encodeFileUrl,
35
+ getMediaService
27
36
  } from "@huyooo/file-explorer-core";
28
37
  import { promises as fs } from "fs";
38
+ import { getPreviewHtmlPath } from "@huyooo/file-explorer-preview/path";
29
39
  var CHANNEL_PREFIX = "file-explorer";
40
+ var PREVIEW_CHANNEL_PREFIX = "file-explorer-preview";
30
41
  var channel = (name) => `${CHANNEL_PREFIX}:${name}`;
42
+ var previewChannel = (name) => `${PREVIEW_CHANNEL_PREFIX}:${name}`;
31
43
  var clipboardAdapter = null;
32
44
  function setClipboardAdapter(adapter) {
33
45
  clipboardAdapter = adapter;
@@ -42,11 +54,11 @@ function createElectronAdapter() {
42
54
  }
43
55
  };
44
56
  }
45
- function electronUrlEncoder(filePath) {
46
- const encodedPath = encodeURIComponent(filePath);
47
- return `app://file${encodedPath}`;
48
- }
49
- function registerFileExplorerHandlers() {
57
+ var electronUrlEncoder = encodeFileUrl;
58
+ var explorerOptions = {};
59
+ var mediaPreviewWindows = /* @__PURE__ */ new Map();
60
+ function registerFileExplorerHandlers(options = {}) {
61
+ explorerOptions = options;
50
62
  const adapter = createElectronAdapter();
51
63
  ipcMain.handle(channel("readDirectory"), async (_event, dirPath) => {
52
64
  const thumbnailService = getThumbnailService();
@@ -210,6 +222,282 @@ function registerFileExplorerHandlers() {
210
222
  }, 100);
211
223
  return { success: true };
212
224
  });
225
+ ipcMain.handle(channel("compressFiles"), async (event, sources, options2) => {
226
+ try {
227
+ const result = await compressFiles(sources, options2, (progress) => {
228
+ event.sender.send(channel("compressProgress"), progress);
229
+ });
230
+ return result;
231
+ } catch (error) {
232
+ return { success: false, error: String(error) };
233
+ }
234
+ });
235
+ ipcMain.handle(channel("extractArchive"), async (event, archivePath, options2) => {
236
+ try {
237
+ const result = await extractArchive(archivePath, options2, (progress) => {
238
+ event.sender.send(channel("extractProgress"), progress);
239
+ });
240
+ return result;
241
+ } catch (error) {
242
+ return { success: false, error: String(error) };
243
+ }
244
+ });
245
+ ipcMain.handle(channel("isArchiveFile"), async (_event, filePath) => {
246
+ return isArchiveFile(filePath);
247
+ });
248
+ const watchUnsubscribers = /* @__PURE__ */ new Map();
249
+ ipcMain.handle(channel("watchDirectory"), async (event, dirPath, watchId) => {
250
+ try {
251
+ const existingUnsubscribe = watchUnsubscribers.get(watchId);
252
+ if (existingUnsubscribe) {
253
+ existingUnsubscribe();
254
+ watchUnsubscribers.delete(watchId);
255
+ }
256
+ const watchManager = getWatchManager();
257
+ const unsubscribe = watchManager.watch(dirPath, (watchEvent) => {
258
+ event.sender.send(channel("watchEvent"), {
259
+ watchId,
260
+ event: watchEvent
261
+ });
262
+ });
263
+ watchUnsubscribers.set(watchId, unsubscribe);
264
+ return { success: true };
265
+ } catch (error) {
266
+ return { success: false, error: String(error) };
267
+ }
268
+ });
269
+ ipcMain.handle(channel("unwatchDirectory"), async (_event, watchId) => {
270
+ const unsubscribe = watchUnsubscribers.get(watchId);
271
+ if (unsubscribe) {
272
+ unsubscribe();
273
+ watchUnsubscribers.delete(watchId);
274
+ }
275
+ return { success: true };
276
+ });
277
+ ipcMain.handle(channel("showFileInfo"), async (_event, filePath) => {
278
+ return await showFileInfo(filePath);
279
+ });
280
+ ipcMain.handle(channel("openInTerminal"), async (_event, dirPath) => {
281
+ return await openInTerminal(dirPath);
282
+ });
283
+ ipcMain.handle(channel("openInEditor"), async (_event, targetPath) => {
284
+ return await openInEditor(targetPath);
285
+ });
286
+ ipcMain.handle(channel("openInNewWindow"), async (_event, folderPath) => {
287
+ try {
288
+ if (explorerOptions.onOpenInNewWindow) {
289
+ explorerOptions.onOpenInNewWindow(folderPath);
290
+ } else {
291
+ await shell.openPath(folderPath);
292
+ }
293
+ return { success: true };
294
+ } catch (error) {
295
+ return { success: false, error: String(error) };
296
+ }
297
+ });
298
+ ipcMain.on(channel("requestWindowFocus"), (event) => {
299
+ const win = BrowserWindow.fromWebContents(event.sender);
300
+ if (win && !win.isFocused()) {
301
+ win.focus();
302
+ }
303
+ });
304
+ ipcMain.handle(channel("mediaNeedsTranscode"), async (_event, filePath) => {
305
+ const mediaService = getMediaService();
306
+ if (!mediaService) {
307
+ return { success: false, error: "\u5A92\u4F53\u670D\u52A1\u672A\u521D\u59CB\u5316" };
308
+ }
309
+ try {
310
+ const info = await mediaService.needsTranscode(filePath);
311
+ return { success: true, data: info };
312
+ } catch (error) {
313
+ return { success: false, error: String(error) };
314
+ }
315
+ });
316
+ ipcMain.handle(channel("mediaGetPlayableUrl"), async (event, filePath) => {
317
+ const mediaService = getMediaService();
318
+ if (!mediaService) {
319
+ return { success: false, error: "\u5A92\u4F53\u670D\u52A1\u672A\u521D\u59CB\u5316" };
320
+ }
321
+ try {
322
+ const url = await mediaService.getPlayableUrl(filePath, (progress) => {
323
+ event.sender.send(channel("mediaTranscodeProgress"), {
324
+ filePath,
325
+ progress
326
+ });
327
+ });
328
+ return { success: true, url };
329
+ } catch (error) {
330
+ return { success: false, error: String(error) };
331
+ }
332
+ });
333
+ ipcMain.handle(channel("mediaGetMetadata"), async (_event, filePath) => {
334
+ const mediaService = getMediaService();
335
+ if (!mediaService) {
336
+ return { success: false, error: "\u5A92\u4F53\u670D\u52A1\u672A\u521D\u59CB\u5316" };
337
+ }
338
+ try {
339
+ const metadata = await mediaService.getMetadata(filePath);
340
+ return { success: true, data: metadata };
341
+ } catch (error) {
342
+ return { success: false, error: String(error) };
343
+ }
344
+ });
345
+ ipcMain.handle(channel("mediaCleanupFile"), async (_event, filePath) => {
346
+ const mediaService = getMediaService();
347
+ if (!mediaService) {
348
+ return { success: false, error: "\u5A92\u4F53\u670D\u52A1\u672A\u521D\u59CB\u5316" };
349
+ }
350
+ try {
351
+ await mediaService.cleanupFile(filePath);
352
+ return { success: true };
353
+ } catch (error) {
354
+ return { success: false, error: String(error) };
355
+ }
356
+ });
357
+ ipcMain.handle(channel("openMediaPreviewWindow"), async (event, filePath, mediaType) => {
358
+ try {
359
+ const fileName = path.basename(filePath);
360
+ const mediaService = getMediaService();
361
+ let fileUrl;
362
+ if (mediaType === "image") {
363
+ fileUrl = electronUrlEncoder(filePath);
364
+ } else if (mediaService) {
365
+ const playableUrl = await mediaService.getPlayableUrl(filePath, (progress) => {
366
+ event.sender.send(channel("mediaTranscodeProgress"), {
367
+ filePath,
368
+ progress
369
+ });
370
+ });
371
+ fileUrl = playableUrl || electronUrlEncoder(filePath);
372
+ } else {
373
+ fileUrl = electronUrlEncoder(filePath);
374
+ }
375
+ const existingWindow = mediaPreviewWindows.get(filePath);
376
+ if (existingWindow && !existingWindow.isDestroyed()) {
377
+ existingWindow.focus();
378
+ return { success: true };
379
+ }
380
+ const isMac = process.platform === "darwin";
381
+ const isWin = process.platform === "win32";
382
+ const titleBarHeight = isMac ? 38 : 32;
383
+ let contentWidth = 720;
384
+ let contentHeight = 405;
385
+ let minContentWidth = 720;
386
+ let minContentHeight = 405;
387
+ if (mediaType === "audio") {
388
+ contentWidth = 720;
389
+ contentHeight = 405;
390
+ minContentWidth = 720;
391
+ minContentHeight = 405;
392
+ } else if (mediaType === "image") {
393
+ contentWidth = 1e3;
394
+ contentHeight = 700;
395
+ minContentWidth = 600;
396
+ minContentHeight = 400;
397
+ }
398
+ const width = contentWidth;
399
+ const height = contentHeight + titleBarHeight;
400
+ const minWidth = minContentWidth;
401
+ const minHeight = minContentHeight + titleBarHeight;
402
+ const previewPreloadPath = path.join(__dirname, "../preload/preview.cjs");
403
+ const previewWindow = new BrowserWindow({
404
+ width,
405
+ height,
406
+ minWidth,
407
+ minHeight,
408
+ title: fileName,
409
+ modal: false,
410
+ show: false,
411
+ backgroundColor: "#1a1a1a",
412
+ // 窗口功能
413
+ resizable: true,
414
+ maximizable: true,
415
+ fullscreenable: true,
416
+ // Windows 和 macOS 都使用无边框样式,自定义标题栏
417
+ frame: isMac ? true : false,
418
+ // macOS 用 hiddenInset,Windows 完全无边框
419
+ titleBarStyle: isMac ? "hiddenInset" : void 0,
420
+ trafficLightPosition: isMac ? { x: 12, y: 12 } : void 0,
421
+ webPreferences: {
422
+ preload: previewPreloadPath,
423
+ nodeIntegration: false,
424
+ contextIsolation: true,
425
+ // 注意:以下设置会触发安全警告,但这是必要的
426
+ // - sandbox: false - 需要加载本地媒体文件(file:// 协议)
427
+ // - webSecurity: false - 允许加载本地文件和 Iconify CDN
428
+ // 这些警告在开发环境中显示,打包后不会显示
429
+ sandbox: false,
430
+ webSecurity: false
431
+ }
432
+ });
433
+ const previewHtmlPath = getPreviewHtmlPath();
434
+ const searchParams = new URLSearchParams({
435
+ type: mediaType,
436
+ url: encodeURIComponent(fileUrl),
437
+ name: encodeURIComponent(fileName),
438
+ platform: isMac ? "darwin" : isWin ? "win32" : "linux"
439
+ });
440
+ previewWindow.loadFile(previewHtmlPath, { search: searchParams.toString() });
441
+ previewWindow.once("ready-to-show", () => {
442
+ previewWindow.show();
443
+ });
444
+ previewWindow.on("maximize", () => {
445
+ previewWindow.webContents.send(previewChannel("maximizeChange"), true);
446
+ });
447
+ previewWindow.on("unmaximize", () => {
448
+ previewWindow.webContents.send(previewChannel("maximizeChange"), false);
449
+ });
450
+ previewWindow.on("closed", () => {
451
+ mediaPreviewWindows.delete(filePath);
452
+ });
453
+ mediaPreviewWindows.set(filePath, previewWindow);
454
+ return { success: true };
455
+ } catch (error) {
456
+ return { success: false, error: String(error) };
457
+ }
458
+ });
459
+ ipcMain.handle(channel("closeMediaPreviewWindow"), async (_event, filePath) => {
460
+ const window = mediaPreviewWindows.get(filePath);
461
+ if (window && !window.isDestroyed()) {
462
+ window.close();
463
+ mediaPreviewWindows.delete(filePath);
464
+ }
465
+ return { success: true };
466
+ });
467
+ ipcMain.on(previewChannel("minimize"), (event) => {
468
+ const win = BrowserWindow.fromWebContents(event.sender);
469
+ if (win) {
470
+ win.minimize();
471
+ }
472
+ });
473
+ ipcMain.on(previewChannel("toggleMaximize"), (event) => {
474
+ const win = BrowserWindow.fromWebContents(event.sender);
475
+ if (win) {
476
+ if (win.isMaximized()) {
477
+ win.unmaximize();
478
+ } else {
479
+ win.maximize();
480
+ }
481
+ }
482
+ });
483
+ ipcMain.on(previewChannel("close"), (event) => {
484
+ const win = BrowserWindow.fromWebContents(event.sender);
485
+ if (win) {
486
+ win.close();
487
+ }
488
+ });
489
+ ipcMain.on(previewChannel("hide"), (event) => {
490
+ const win = BrowserWindow.fromWebContents(event.sender);
491
+ if (win) {
492
+ win.hide();
493
+ }
494
+ });
495
+ ipcMain.on(previewChannel("show"), (event) => {
496
+ const win = BrowserWindow.fromWebContents(event.sender);
497
+ if (win) {
498
+ win.show();
499
+ }
500
+ });
213
501
  console.log("\u2705 File Explorer IPC handlers registered");
214
502
  }
215
503
  export {
@@ -56,7 +56,53 @@ function createFileExplorerAPI() {
56
56
  };
57
57
  import_electron.ipcRenderer.on(channel("searchResults"), handler);
58
58
  return () => import_electron.ipcRenderer.removeListener(channel("searchResults"), handler);
59
- }
59
+ },
60
+ compressFiles: (sources, options) => import_electron.ipcRenderer.invoke(channel("compressFiles"), sources, options),
61
+ extractArchive: (archivePath, options) => import_electron.ipcRenderer.invoke(channel("extractArchive"), archivePath, options),
62
+ isArchiveFile: (filePath) => import_electron.ipcRenderer.invoke(channel("isArchiveFile"), filePath),
63
+ onCompressProgress: (callback) => {
64
+ const handler = (_event, data) => {
65
+ callback(data);
66
+ };
67
+ import_electron.ipcRenderer.on(channel("compressProgress"), handler);
68
+ return () => import_electron.ipcRenderer.removeListener(channel("compressProgress"), handler);
69
+ },
70
+ onExtractProgress: (callback) => {
71
+ const handler = (_event, data) => {
72
+ callback(data);
73
+ };
74
+ import_electron.ipcRenderer.on(channel("extractProgress"), handler);
75
+ return () => import_electron.ipcRenderer.removeListener(channel("extractProgress"), handler);
76
+ },
77
+ watchDirectory: (dirPath, watchId) => import_electron.ipcRenderer.invoke(channel("watchDirectory"), dirPath, watchId),
78
+ unwatchDirectory: (watchId) => import_electron.ipcRenderer.invoke(channel("unwatchDirectory"), watchId),
79
+ onWatchEvent: (callback) => {
80
+ const handler = (_event, data) => {
81
+ callback(data);
82
+ };
83
+ import_electron.ipcRenderer.on(channel("watchEvent"), handler);
84
+ return () => import_electron.ipcRenderer.removeListener(channel("watchEvent"), handler);
85
+ },
86
+ showFileInfo: (filePath) => import_electron.ipcRenderer.invoke(channel("showFileInfo"), filePath),
87
+ openInTerminal: (dirPath) => import_electron.ipcRenderer.invoke(channel("openInTerminal"), dirPath),
88
+ openInEditor: (targetPath) => import_electron.ipcRenderer.invoke(channel("openInEditor"), targetPath),
89
+ openInNewWindow: (folderPath) => import_electron.ipcRenderer.invoke(channel("openInNewWindow"), folderPath),
90
+ requestWindowFocus: () => import_electron.ipcRenderer.send(channel("requestWindowFocus")),
91
+ // ===== 媒体服务 API =====
92
+ mediaNeedsTranscode: (filePath) => import_electron.ipcRenderer.invoke(channel("mediaNeedsTranscode"), filePath),
93
+ mediaGetPlayableUrl: (filePath) => import_electron.ipcRenderer.invoke(channel("mediaGetPlayableUrl"), filePath),
94
+ mediaGetMetadata: (filePath) => import_electron.ipcRenderer.invoke(channel("mediaGetMetadata"), filePath),
95
+ onMediaTranscodeProgress: (callback) => {
96
+ const handler = (_event, data) => {
97
+ callback(data);
98
+ };
99
+ import_electron.ipcRenderer.on(channel("mediaTranscodeProgress"), handler);
100
+ return () => import_electron.ipcRenderer.removeListener(channel("mediaTranscodeProgress"), handler);
101
+ },
102
+ mediaCleanupFile: (filePath) => import_electron.ipcRenderer.invoke(channel("mediaCleanupFile"), filePath),
103
+ // ===== 媒体预览窗口 API =====
104
+ openMediaPreviewWindow: (filePath, mediaType) => import_electron.ipcRenderer.invoke(channel("openMediaPreviewWindow"), filePath, mediaType),
105
+ closeMediaPreviewWindow: (filePath) => import_electron.ipcRenderer.invoke(channel("closeMediaPreviewWindow"), filePath)
60
106
  };
61
107
  }
62
108
  function exposeFileExplorerAPI(apiName = "fileExplorerAPI") {
@@ -126,6 +126,139 @@ interface FileExplorerAPI {
126
126
  items: unknown[];
127
127
  done: boolean;
128
128
  }) => void) => () => void;
129
+ /** 压缩文件 */
130
+ compressFiles: (sources: string[], options: {
131
+ format: 'zip' | 'tar' | 'tgz' | 'tarbz2';
132
+ level?: 'fast' | 'normal' | 'best';
133
+ outputName: string;
134
+ outputDir: string;
135
+ deleteSource?: boolean;
136
+ }) => Promise<{
137
+ success: boolean;
138
+ outputPath?: string;
139
+ error?: string;
140
+ }>;
141
+ /** 解压文件 */
142
+ extractArchive: (archivePath: string, options: {
143
+ targetDir: string;
144
+ deleteArchive?: boolean;
145
+ }) => Promise<{
146
+ success: boolean;
147
+ outputPath?: string;
148
+ error?: string;
149
+ }>;
150
+ /** 判断是否为压缩文件 */
151
+ isArchiveFile: (filePath: string) => Promise<boolean>;
152
+ /** 监听压缩进度 */
153
+ onCompressProgress: (callback: (data: {
154
+ currentFile: string;
155
+ processedCount: number;
156
+ totalCount: number;
157
+ percent: number;
158
+ }) => void) => () => void;
159
+ /** 监听解压进度 */
160
+ onExtractProgress: (callback: (data: {
161
+ currentFile: string;
162
+ processedCount: number;
163
+ totalCount: number;
164
+ percent: number;
165
+ }) => void) => () => void;
166
+ /** 监听目录变化 */
167
+ watchDirectory: (dirPath: string, watchId: string) => Promise<{
168
+ success: boolean;
169
+ error?: string;
170
+ }>;
171
+ /** 停止监听目录 */
172
+ unwatchDirectory: (watchId: string) => Promise<{
173
+ success: boolean;
174
+ }>;
175
+ /** 监听文件变化事件 */
176
+ onWatchEvent: (callback: (data: {
177
+ watchId: string;
178
+ event: {
179
+ type: 'add' | 'change' | 'remove' | 'rename';
180
+ path: string;
181
+ filename: string;
182
+ };
183
+ }) => void) => () => void;
184
+ /** 显示文件/文件夹的系统属性窗口 */
185
+ showFileInfo: (filePath: string) => Promise<{
186
+ success: boolean;
187
+ error?: string;
188
+ }>;
189
+ /** 在终端中打开目录 */
190
+ openInTerminal: (dirPath: string) => Promise<{
191
+ success: boolean;
192
+ error?: string;
193
+ }>;
194
+ /** 通过 Cursor 打开 */
195
+ openInEditor: (targetPath: string) => Promise<{
196
+ success: boolean;
197
+ error?: string;
198
+ }>;
199
+ /** 在新窗口中打开文件夹(使用系统文件管理器) */
200
+ openInNewWindow: (folderPath: string) => Promise<{
201
+ success: boolean;
202
+ error?: string;
203
+ }>;
204
+ /** 请求窗口聚焦(用于右键打开菜单前激活窗口) */
205
+ requestWindowFocus: () => void;
206
+ /** 检测媒体文件是否需要转码 */
207
+ mediaNeedsTranscode: (filePath: string) => Promise<{
208
+ success: boolean;
209
+ data?: {
210
+ type: 'video' | 'audio';
211
+ needsTranscode: boolean;
212
+ method: 'direct' | 'remux' | 'transcode';
213
+ estimatedTime?: number;
214
+ targetFormat?: string;
215
+ };
216
+ error?: string;
217
+ }>;
218
+ /** 获取可播放的媒体 URL(自动转码) */
219
+ mediaGetPlayableUrl: (filePath: string) => Promise<{
220
+ success: boolean;
221
+ url?: string;
222
+ error?: string;
223
+ }>;
224
+ /** 获取媒体元数据 */
225
+ mediaGetMetadata: (filePath: string) => Promise<{
226
+ success: boolean;
227
+ data?: {
228
+ filePath: string;
229
+ type: 'video' | 'audio';
230
+ duration: number;
231
+ title?: string;
232
+ artist?: string;
233
+ album?: string;
234
+ };
235
+ error?: string;
236
+ }>;
237
+ /** 监听转码进度 */
238
+ onMediaTranscodeProgress: (callback: (data: {
239
+ filePath: string;
240
+ progress: {
241
+ percent: number;
242
+ time?: number;
243
+ duration?: number;
244
+ speed?: string;
245
+ };
246
+ }) => void) => () => void;
247
+ /** 清理指定文件的转码缓存 */
248
+ mediaCleanupFile: (filePath: string) => Promise<{
249
+ success: boolean;
250
+ error?: string;
251
+ }>;
252
+ /** 打开媒体预览窗口 */
253
+ openMediaPreviewWindow: (filePath: string, mediaType: 'image' | 'video' | 'audio') => Promise<{
254
+ success: boolean;
255
+ error?: string;
256
+ }>;
257
+ /** 关闭媒体预览窗口 */
258
+ closeMediaPreviewWindow: (filePath: string) => Promise<{
259
+ success: boolean;
260
+ error?: string;
261
+ }>;
129
262
  }
130
263
  /**
131
264
  * 创建文件操作 API
@@ -126,6 +126,139 @@ interface FileExplorerAPI {
126
126
  items: unknown[];
127
127
  done: boolean;
128
128
  }) => void) => () => void;
129
+ /** 压缩文件 */
130
+ compressFiles: (sources: string[], options: {
131
+ format: 'zip' | 'tar' | 'tgz' | 'tarbz2';
132
+ level?: 'fast' | 'normal' | 'best';
133
+ outputName: string;
134
+ outputDir: string;
135
+ deleteSource?: boolean;
136
+ }) => Promise<{
137
+ success: boolean;
138
+ outputPath?: string;
139
+ error?: string;
140
+ }>;
141
+ /** 解压文件 */
142
+ extractArchive: (archivePath: string, options: {
143
+ targetDir: string;
144
+ deleteArchive?: boolean;
145
+ }) => Promise<{
146
+ success: boolean;
147
+ outputPath?: string;
148
+ error?: string;
149
+ }>;
150
+ /** 判断是否为压缩文件 */
151
+ isArchiveFile: (filePath: string) => Promise<boolean>;
152
+ /** 监听压缩进度 */
153
+ onCompressProgress: (callback: (data: {
154
+ currentFile: string;
155
+ processedCount: number;
156
+ totalCount: number;
157
+ percent: number;
158
+ }) => void) => () => void;
159
+ /** 监听解压进度 */
160
+ onExtractProgress: (callback: (data: {
161
+ currentFile: string;
162
+ processedCount: number;
163
+ totalCount: number;
164
+ percent: number;
165
+ }) => void) => () => void;
166
+ /** 监听目录变化 */
167
+ watchDirectory: (dirPath: string, watchId: string) => Promise<{
168
+ success: boolean;
169
+ error?: string;
170
+ }>;
171
+ /** 停止监听目录 */
172
+ unwatchDirectory: (watchId: string) => Promise<{
173
+ success: boolean;
174
+ }>;
175
+ /** 监听文件变化事件 */
176
+ onWatchEvent: (callback: (data: {
177
+ watchId: string;
178
+ event: {
179
+ type: 'add' | 'change' | 'remove' | 'rename';
180
+ path: string;
181
+ filename: string;
182
+ };
183
+ }) => void) => () => void;
184
+ /** 显示文件/文件夹的系统属性窗口 */
185
+ showFileInfo: (filePath: string) => Promise<{
186
+ success: boolean;
187
+ error?: string;
188
+ }>;
189
+ /** 在终端中打开目录 */
190
+ openInTerminal: (dirPath: string) => Promise<{
191
+ success: boolean;
192
+ error?: string;
193
+ }>;
194
+ /** 通过 Cursor 打开 */
195
+ openInEditor: (targetPath: string) => Promise<{
196
+ success: boolean;
197
+ error?: string;
198
+ }>;
199
+ /** 在新窗口中打开文件夹(使用系统文件管理器) */
200
+ openInNewWindow: (folderPath: string) => Promise<{
201
+ success: boolean;
202
+ error?: string;
203
+ }>;
204
+ /** 请求窗口聚焦(用于右键打开菜单前激活窗口) */
205
+ requestWindowFocus: () => void;
206
+ /** 检测媒体文件是否需要转码 */
207
+ mediaNeedsTranscode: (filePath: string) => Promise<{
208
+ success: boolean;
209
+ data?: {
210
+ type: 'video' | 'audio';
211
+ needsTranscode: boolean;
212
+ method: 'direct' | 'remux' | 'transcode';
213
+ estimatedTime?: number;
214
+ targetFormat?: string;
215
+ };
216
+ error?: string;
217
+ }>;
218
+ /** 获取可播放的媒体 URL(自动转码) */
219
+ mediaGetPlayableUrl: (filePath: string) => Promise<{
220
+ success: boolean;
221
+ url?: string;
222
+ error?: string;
223
+ }>;
224
+ /** 获取媒体元数据 */
225
+ mediaGetMetadata: (filePath: string) => Promise<{
226
+ success: boolean;
227
+ data?: {
228
+ filePath: string;
229
+ type: 'video' | 'audio';
230
+ duration: number;
231
+ title?: string;
232
+ artist?: string;
233
+ album?: string;
234
+ };
235
+ error?: string;
236
+ }>;
237
+ /** 监听转码进度 */
238
+ onMediaTranscodeProgress: (callback: (data: {
239
+ filePath: string;
240
+ progress: {
241
+ percent: number;
242
+ time?: number;
243
+ duration?: number;
244
+ speed?: string;
245
+ };
246
+ }) => void) => () => void;
247
+ /** 清理指定文件的转码缓存 */
248
+ mediaCleanupFile: (filePath: string) => Promise<{
249
+ success: boolean;
250
+ error?: string;
251
+ }>;
252
+ /** 打开媒体预览窗口 */
253
+ openMediaPreviewWindow: (filePath: string, mediaType: 'image' | 'video' | 'audio') => Promise<{
254
+ success: boolean;
255
+ error?: string;
256
+ }>;
257
+ /** 关闭媒体预览窗口 */
258
+ closeMediaPreviewWindow: (filePath: string) => Promise<{
259
+ success: boolean;
260
+ error?: string;
261
+ }>;
129
262
  }
130
263
  /**
131
264
  * 创建文件操作 API