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 {