Skip to content

Commit dd2d29b

Browse files
committed
feat: support drop folder
1 parent 105de02 commit dd2d29b

File tree

4 files changed

+140
-55
lines changed

4 files changed

+140
-55
lines changed

main/helpers/ipcHandlers.ts

Lines changed: 95 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,70 @@
11
import { ipcMain, BrowserWindow, dialog, shell } from 'electron';
22
import * as fs from 'fs';
3+
import * as path from 'path';
34
import { createMessageSender } from './messageHandler';
45

6+
// 定义支持的文件扩展名常量
7+
export const MEDIA_EXTENSIONS = [
8+
// 视频格式
9+
'.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv', '.webm',
10+
// 音频格式
11+
'.mp3', '.wav', '.ogg', '.aac', '.wma', '.flac', '.m4a',
12+
'.aiff', '.ape', '.opus', '.ac3', '.amr', '.au', '.mid',
13+
// 其他常见视频格式
14+
'.3gp', '.asf', '.rm', '.rmvb', '.vob', '.ts', '.mts', '.m2ts',
15+
];
16+
17+
export const SUBTITLE_EXTENSIONS = [
18+
// 字幕格式
19+
'.srt', '.vtt', '.ass', '.ssa',
20+
];
21+
22+
// 判断文件是否为媒体文件
23+
export function isMediaFile(filePath: string): boolean {
24+
const ext = path.extname(filePath).toLowerCase();
25+
return MEDIA_EXTENSIONS.includes(ext);
26+
}
27+
28+
// 判断文件是否为字幕文件
29+
export function isSubtitleFile(filePath: string): boolean {
30+
const ext = path.extname(filePath).toLowerCase();
31+
return SUBTITLE_EXTENSIONS.includes(ext);
32+
}
33+
34+
// 递归获取文件夹中的符合任务类型的文件
35+
async function getMediaFilesFromDirectory(directoryPath: string, taskType: string): Promise<string[]> {
36+
// 根据任务类型选择扩展名
37+
const supportedExtensions = taskType === 'translate'
38+
? SUBTITLE_EXTENSIONS
39+
: MEDIA_EXTENSIONS;
40+
41+
const files: string[] = [];
42+
43+
try {
44+
const entries = await fs.promises.readdir(directoryPath, { withFileTypes: true });
45+
46+
for (const entry of entries) {
47+
const fullPath = path.join(directoryPath, entry.name);
48+
49+
if (entry.isDirectory()) {
50+
// 递归处理子目录
51+
const subDirFiles = await getMediaFilesFromDirectory(fullPath, taskType);
52+
files.push(...subDirFiles);
53+
} else if (entry.isFile()) {
54+
// 检查文件扩展名是否受支持
55+
const ext = path.extname(entry.name).toLowerCase();
56+
if (supportedExtensions.includes(ext)) {
57+
files.push(fullPath);
58+
}
59+
}
60+
}
61+
} catch (error) {
62+
console.error(`读取目录 ${directoryPath} 时出错:`, error);
63+
}
64+
65+
return files;
66+
}
67+
568
export function setupIpcHandlers(mainWindow: BrowserWindow) {
669
ipcMain.on('message', async (event, arg) => {
770
event.reply('message', `${arg} World!`);
@@ -11,40 +74,12 @@ export function setupIpcHandlers(mainWindow: BrowserWindow) {
1174
const { fileType } = data;
1275
console.log(fileType, 'fileType');
1376
const name = fileType === 'srt' ? 'Subtitle Files' : 'Media Files';
14-
const extensions =
15-
fileType === 'srt'
16-
? ['srt']
17-
: [
18-
'mp4',
19-
'avi',
20-
'mov',
21-
'mkv',
22-
'flv',
23-
'wmv',
24-
'webm',
25-
'mp3',
26-
'wav',
27-
'ogg',
28-
'aac',
29-
'wma',
30-
'flac',
31-
'm4a',
32-
'aiff',
33-
'ape',
34-
'opus',
35-
'ac3',
36-
'amr',
37-
'au',
38-
'mid',
39-
'3gp',
40-
'asf',
41-
'rm',
42-
'rmvb',
43-
'vob',
44-
'ts',
45-
'mts',
46-
'm2ts',
47-
];
77+
78+
// 使用已定义的常量获取扩展名
79+
const extensions = fileType === 'srt'
80+
? SUBTITLE_EXTENSIONS.map(ext => ext.substring(1)) // 移除前面的点
81+
: MEDIA_EXTENSIONS.map(ext => ext.substring(1));
82+
4883
const result = await dialog.showOpenDialog({
4984
properties: ['openFile', 'multiSelections'],
5085
filters: [
@@ -69,20 +104,33 @@ export function setupIpcHandlers(mainWindow: BrowserWindow) {
69104
shell.openExternal(url);
70105
});
71106

72-
ipcMain.handle('getDroppedFiles', async (event, files) => {
73-
// 验证文件是否存在和可访问
74-
const validPaths = await Promise.all(
75-
files.map(async (filePath) => {
76-
try {
77-
await fs.promises.access(filePath);
78-
return filePath;
79-
} catch {
80-
return null;
107+
ipcMain.handle('getDroppedFiles', async (event, { files, taskType }) => {
108+
// 处理文件和文件夹
109+
const allValidPaths: string[] = [];
110+
111+
for (const filePath of files) {
112+
try {
113+
const stats = await fs.promises.stat(filePath);
114+
115+
if (stats.isDirectory()) {
116+
// 如果是文件夹,递归获取所有符合任务类型的文件
117+
const filteredFiles = await getMediaFilesFromDirectory(filePath, taskType);
118+
allValidPaths.push(...filteredFiles);
119+
} else if (stats.isFile()) {
120+
// 如果是文件,根据任务类型过滤
121+
// 根据任务类型决定添加哪种文件
122+
if ((taskType === 'translate' && isSubtitleFile(filePath)) ||
123+
(taskType !== 'translate' && isMediaFile(filePath))) {
124+
allValidPaths.push(filePath);
125+
}
81126
}
82-
}),
83-
);
84-
85-
return validPaths.filter(Boolean);
127+
} catch {
128+
// 如果访问失败,跳过此路径
129+
continue;
130+
}
131+
}
132+
133+
return allValidPaths;
86134
});
87135

88136
ipcMain.handle('selectDirectory', async () => {

renderer/pages/[locale]/home.tsx

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,48 @@ export default function Component() {
4747
e.preventDefault();
4848
setIsDragging(false);
4949

50-
const droppedFiles = filterSupportedFiles(Array.from(e.dataTransfer.files));
50+
// 获取所有拖放的项目(文件和文件夹)
51+
const items = e.dataTransfer.items;
52+
const paths: string[] = [];
5153

52-
if (droppedFiles.length > 0) {
53-
const fileList = droppedFiles.map((file) => file.path);
54-
window?.ipc?.invoke('getDroppedFiles', fileList).then((filePaths) => {
54+
if (items) {
55+
// 使用DataTransferItemList接口获取所有文件和文件夹
56+
for (let i = 0; i < items.length; i++) {
57+
const item = items[i];
58+
// 如果是文件系统条目
59+
if (item.kind === 'file') {
60+
const fileSystemEntry = item.webkitGetAsEntry();
61+
// 将文件或文件夹的路径添加到列表中
62+
if (fileSystemEntry) {
63+
// @ts-ignore - 获取文件路径,这是Electron特有的属性
64+
const path = item.getAsFile()?.path;
65+
if (path) {
66+
paths.push(path);
67+
}
68+
}
69+
}
70+
}
71+
} else {
72+
// 回退到标准File API(不支持WebkitGetAsEntry的情况)
73+
const files = e.dataTransfer.files;
74+
for (let i = 0; i < files.length; i++) {
75+
// @ts-ignore - 获取文件路径,这是Electron特有的属性
76+
const path = files[i].path;
77+
if (path) {
78+
paths.push(path);
79+
}
80+
}
81+
}
82+
83+
if (paths.length > 0) {
84+
// 根据translateOnly任务类型决定是否只处理字幕文件
85+
const isTranslateOnly = formData.taskType === 'translateOnly';
86+
const taskType = isTranslateOnly ? 'translate' : 'media';
87+
88+
window?.ipc?.invoke('getDroppedFiles', {
89+
files: paths,
90+
taskType
91+
}).then((filePaths) => {
5592
const newFiles = filePaths.map((filePath) => ({
5693
uuid: Math.random().toString(36).substring(2),
5794
filePath,

renderer/public/locales/en/home.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@
7070
"noLimit": "Maximize context semantic(-1)",
7171
"noContext": "Minimize subtitle repetition(0)",
7272
"importSubtitles": "Import Subtitles",
73-
"dragSubtitleHere": "Drag subtitle files here, or click to upload",
74-
"dragMediaHere": "Drag media files here, or click to upload",
73+
"dragSubtitleHere": "Drag subtitle files or folders here, or click to upload",
74+
"dragMediaHere": "Drag media files or folders here, or click to upload",
7575
"supportedSubtitleFormats": "Supported subtitle formats: SRT",
7676
"supportedMediaFormats": "Supported media formats: MP4, AVI, MKV, MOV, MP3, WAV"
7777
}

renderer/public/locales/zh/home.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@
7676
"noLimit": "尽可能关联上下文语义(-1)",
7777
"noContext": "尽可能减少字幕重复问题(0)",
7878
"importSubtitles": "导入字幕",
79-
"dragSubtitleHere": "拖放字幕文件到这里,或者点击上传",
80-
"dragMediaHere": "拖放媒体文件到这里,或者点击上传",
79+
"dragSubtitleHere": "拖放字幕文件或目录到这里,或者点击上传",
80+
"dragMediaHere": "拖放媒体文件或目录到这里,或者点击上传",
8181
"supportedSubtitleFormats": "支持的字幕格式:SRT",
8282
"supportedMediaFormats": "支持的媒体格式:MP4, AVI, MKV, MOV, MP3, WAV"
8383
}

0 commit comments

Comments
 (0)