diff --git a/green-resources-manager/public/js/ipc/file-handlers.js b/green-resources-manager/public/js/ipc/file-handlers.js
index 5de2deb..07b6985 100644
--- a/green-resources-manager/public/js/ipc/file-handlers.js
+++ b/green-resources-manager/public/js/ipc/file-handlers.js
@@ -173,6 +173,45 @@ function registerIpcHandlers(ipcMain, fileUtils, pathUtils) {
}
})
+ // Recursively scan a directory for files with given extensions
+ ipcMain.handle('scan-directory-recursively', async (event, directoryPath, fileExtensions) => {
+ try {
+ if (!directoryPath || directoryPath.trim() === '') {
+ return { success: false, error: 'Invalid directory path' };
+ }
+
+ const absolutePath = pathUtils ? pathUtils.normalizePath(directoryPath) : path.resolve(directoryPath);
+
+ if (!fs.existsSync(absolutePath)) {
+ return { success: false, error: `Directory does not exist: ${absolutePath}` };
+ }
+
+ const foundFiles = [];
+ const lowerCaseExtensions = new Set(fileExtensions.map(ext => ext.toLowerCase()));
+
+ const scan = (currentPath) => {
+ const items = fs.readdirSync(currentPath, { withFileTypes: true });
+ for (const item of items) {
+ const itemPath = path.join(currentPath, item.name);
+ if (item.isDirectory()) {
+ scan(itemPath);
+ } else if (item.isFile()) {
+ const extension = path.extname(item.name).toLowerCase();
+ if (lowerCaseExtensions.has(extension)) {
+ foundFiles.push(itemPath);
+ }
+ }
+ }
+ };
+
+ scan(absolutePath);
+ return { success: true, files: foundFiles };
+ } catch (error) {
+ console.error('Failed to recursively scan directory:', error);
+ return { success: false, error: error.message };
+ }
+ });
+
// 列出指定文件夹下的图片文件
ipcMain.handle('list-image-files', async (event, folderPath) => {
try {
diff --git a/green-resources-manager/public/preload.js b/green-resources-manager/public/preload.js
index c0dbf97..8407da2 100644
--- a/green-resources-manager/public/preload.js
+++ b/green-resources-manager/public/preload.js
@@ -54,6 +54,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
listImageFiles: (folderPath) => ipcRenderer.invoke('list-image-files', folderPath),
getFolderSize: (filePath) => ipcRenderer.invoke('get-folder-size', filePath),
checkFileExists: (filePath) => ipcRenderer.invoke('check-file-exists', filePath),
+ scanDirectoryRecursively: (directoryPath, fileExtensions) => ipcRenderer.invoke('scan-directory-recursively', directoryPath, fileExtensions),
// 文件URL处理
getFileUrl: (filePath) => ipcRenderer.invoke('get-file-url', filePath),
diff --git a/green-resources-manager/src/components/BaseView.vue b/green-resources-manager/src/components/BaseView.vue
index 90ba7ae..a786adc 100644
--- a/green-resources-manager/src/components/BaseView.vue
+++ b/green-resources-manager/src/components/BaseView.vue
@@ -8,6 +8,7 @@
:sort-by="sortBy"
:add-button-text="toolbarConfig.addButtonText"
:add-folder-button-text="toolbarConfig.addFolderButtonText"
+ :import-folder-button-text="toolbarConfig.importFolderButtonText"
:import-bookmark-button-text="toolbarConfig.importBookmarkButtonText"
:search-placeholder="toolbarConfig.searchPlaceholder"
:sort-options="toolbarConfig.sortOptions"
@@ -16,6 +17,7 @@
:show-layout-control="showLayoutControl"
@add-item="handleAddItem"
@add-folder="handleAddFolder"
+ @import-folder="$emit('import-folder')"
@import-bookmark="handleImportBookmark"
@update:searchQuery="handleSearchQueryUpdate"
@update:sortBy="handleSortByUpdate"
@@ -140,6 +142,7 @@ export default {
'empty-state-action',
'add-item',
'add-folder',
+ 'import-folder',
'import-bookmark',
'sort-changed',
'search-query-changed',
diff --git a/green-resources-manager/src/components/Toolbar.vue b/green-resources-manager/src/components/Toolbar.vue
index 25a8006..36bd17d 100644
--- a/green-resources-manager/src/components/Toolbar.vue
+++ b/green-resources-manager/src/components/Toolbar.vue
@@ -17,6 +17,14 @@
>
{{ addFolderButtonText }}
+
+ {{ importFolderButtonText }}
+
=> {
+ try {
+ // 1. Let user select a folder
+ // @ts-ignore
+ const result = await window.electronAPI.selectFolder();
+ if (!result || !result.success || !result.path) {
+ console.log('Folder selection cancelled.');
+ return;
+ }
+ const directoryPath = result.path;
+
+ // 2. Scan for new video files
+ const manager = initVideoManager();
+ if (!manager) {
+ throw new Error('Video manager is not initialized.');
+ }
+ notify.toast('info', '正在扫描文件夹...', `正在扫描 ${directoryPath}`);
+ const newFiles = await manager.scanDirectoryForNewVideos(directoryPath);
+
+ // 3. Check results and provide feedback
+ if (newFiles.length === 0) {
+ notify.toast('info', '扫描完成', '没有发现新的视频或图片文件。');
+ return;
+ }
+
+ notify.toast('info', '正在导入...', `发现 ${newFiles.length} 个新文件,正在添加到媒体库...`);
+
+ // 4. Batch add new videos
+ const addedCount = await manager.addVideosInBatch(newFiles);
+
+ // 5. Reload and provide final feedback
+ await loadVideos();
+ notify.toast('success', '导入完成', `成功添加 ${addedCount} 个新视频到媒体库。`);
+
+ } catch (error) {
+ console.error('Failed to import from directory:', error);
+ notify.toast('error', '导入失败', `发生错误: ${error.message}`);
+ }
+ };
+
return {
videos,
videoManager,
@@ -255,7 +298,8 @@ export function useVideoManagement(pageId: string = 'videos') {
checkFileExistence,
checkVideoCollectionAchievements,
getVideoManager,
- initVideoManager
+ initVideoManager,
+ importFromDirectory
}
}
diff --git a/green-resources-manager/src/pages/resources/VideoView.vue b/green-resources-manager/src/pages/resources/VideoView.vue
index 4751935..a058f28 100644
--- a/green-resources-manager/src/pages/resources/VideoView.vue
+++ b/green-resources-manager/src/pages/resources/VideoView.vue
@@ -12,6 +12,7 @@
@empty-state-action="handleEmptyStateAction"
@add-item="showAddVideoDialog"
@add-folder="showAddFolderDialog"
+ @import-folder="handleImportFolder"
@sort-changed="handleSortChanged"
@search-query-changed="handleSearchQueryChanged"
@sort-by-changed="handleSortByChanged"
@@ -418,6 +419,7 @@ export default {
videoToolbarConfig: {
addButtonText: '添加视频',
addFolderButtonText: '添加文件夹',
+ importFolderButtonText: '导入文件夹',
searchPlaceholder: '搜索视频...',
sortOptions: [
{ value: 'name', label: '按名称排序' },
@@ -1837,6 +1839,15 @@ export default {
this.showAddVideoDialog()
}
},
+
+ handleImportFolder() {
+ // The 'importFromDirectory' method is exposed from the setup function
+ if (this.importFromDirectory) {
+ this.importFromDirectory();
+ } else {
+ console.error('importFromDirectory function is not available.');
+ }
+ },
// 处理搜索查询变化
handleSearchQueryChanged(newValue) {
diff --git a/green-resources-manager/src/utils/VideoManager.ts b/green-resources-manager/src/utils/VideoManager.ts
index 67bd3de..9da6dd5 100644
--- a/green-resources-manager/src/utils/VideoManager.ts
+++ b/green-resources-manager/src/utils/VideoManager.ts
@@ -104,6 +104,55 @@ class VideoManager {
return video
}
+ // Batch add videos from a list of file paths
+ async addVideosInBatch(filePaths: string[]): Promise {
+ const deriveNameFromPath = (filePath) => {
+ if (!filePath) return '';
+ const normalized = filePath.replace(/\\/g, '/');
+ const filename = normalized.substring(normalized.lastIndexOf('/') + 1);
+ const dotIndex = filename.lastIndexOf('.');
+ return dotIndex > 0 ? filename.substring(0, dotIndex) : filename;
+ };
+
+ let addedCount = 0;
+ for (const filePath of filePaths) {
+ // Basic check to avoid duplicates if called incorrectly
+ if (this.videos.some(v => v.filePath === filePath)) {
+ continue;
+ }
+
+ const video = {
+ id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, // More unique ID for batches
+ name: deriveNameFromPath(filePath) || '未知视频',
+ description: '',
+ tags: [],
+ actors: [],
+ series: '',
+ director: '',
+ genre: '',
+ year: '',
+ duration: 0,
+ filePath: filePath,
+ thumbnail: '',
+ watchProgress: 0,
+ watchCount: 0,
+ lastWatched: null,
+ firstWatched: null,
+ addedDate: new Date().toISOString(),
+ rating: 0,
+ notes: ''
+ };
+ this.videos.push(video);
+ addedCount++;
+ }
+
+ if (addedCount > 0) {
+ await this.saveVideos();
+ }
+
+ return addedCount;
+ }
+
// 更新视频
async updateVideo(id, videoData) {
const index = this.videos.findIndex(video => video.id === id)
@@ -259,6 +308,42 @@ class VideoManager {
}
}
+ // Scan a directory recursively for new video and image files
+ async scanDirectoryForNewVideos(directoryPath: string): Promise {
+ const mediaExtensions = [
+ // Video formats
+ '.mp4', '.mkv', '.avi', '.mov', '.wmv', '.flv', '.webm', '.mpg', '.mpeg',
+ // Image formats (useful for covers/thumbnails)
+ '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'
+ ];
+
+ try {
+ // @ts-ignore
+ if (!window.electronAPI || !window.electronAPI.scanDirectoryRecursively) {
+ throw new Error('The scanning functionality is not available in the current environment.');
+ }
+
+ // @ts-ignore
+ const result = await window.electronAPI.scanDirectoryRecursively(directoryPath, mediaExtensions);
+
+ if (result.success) {
+ // Create a Set of existing file paths for efficient lookup
+ const existingPaths = new Set(this.videos.map(video => video.filePath));
+
+ // Filter out files that already exist in the library
+ const newFiles = result.files.filter(filePath => !existingPaths.has(filePath));
+
+ console.log(`Found ${result.files.length} media files, of which ${newFiles.length} are new.`);
+ return newFiles;
+ } else {
+ throw new Error(result.error || 'An unknown error occurred during the scan.');
+ }
+ } catch (error) {
+ console.error(`Error scanning directory ${directoryPath}:`, error);
+ throw error;
+ }
+ }
+
// 保存缩略图为本地文件
async saveThumbnailToFile(filename, dataUrl) {
try {