Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions green-resources-manager/public/js/ipc/file-handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions green-resources-manager/public/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
3 changes: 3 additions & 0 deletions green-resources-manager/src/components/BaseView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -140,6 +142,7 @@ export default {
'empty-state-action',
'add-item',
'add-folder',
'import-folder',
'import-bookmark',
'sort-changed',
'search-query-changed',
Expand Down
13 changes: 13 additions & 0 deletions green-resources-manager/src/components/Toolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@
>
{{ addFolderButtonText }}
</fun-button>
<fun-button
v-if="importFolderButtonText"
type="secondary"
icon="📥"
@click="$emit('import-folder')"
>
{{ importFolderButtonText }}
</fun-button>
<fun-button
v-if="importBookmarkButtonText"
type="secondary"
Expand Down Expand Up @@ -83,6 +91,10 @@ export default {
type: String,
default: ''
},
importFolderButtonText: {
type: String,
default: ''
},
importBookmarkButtonText: {
type: String,
default: ''
Expand Down Expand Up @@ -120,6 +132,7 @@ export default {
emits: [
'add-item',
'add-folder',
'import-folder',
'import-bookmark',
'update:searchQuery',
'update:sortBy',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,49 @@ export function useVideoManagement(pageId: string = 'videos') {
return videoManager.value
}

/**
* Imports new videos from a selected directory.
*/
const importFromDirectory = async (): Promise<void> => {
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,
Expand All @@ -255,7 +298,8 @@ export function useVideoManagement(pageId: string = 'videos') {
checkFileExistence,
checkVideoCollectionAchievements,
getVideoManager,
initVideoManager
initVideoManager,
importFromDirectory
}
}

11 changes: 11 additions & 0 deletions green-resources-manager/src/pages/resources/VideoView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -418,6 +419,7 @@ export default {
videoToolbarConfig: {
addButtonText: '添加视频',
addFolderButtonText: '添加文件夹',
importFolderButtonText: '导入文件夹',
searchPlaceholder: '搜索视频...',
sortOptions: [
{ value: 'name', label: '按名称排序' },
Expand Down Expand Up @@ -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) {
Expand Down
85 changes: 85 additions & 0 deletions green-resources-manager/src/utils/VideoManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,55 @@ class VideoManager {
return video
}

// Batch add videos from a list of file paths
async addVideosInBatch(filePaths: string[]): Promise<number> {
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)
Expand Down Expand Up @@ -259,6 +308,42 @@ class VideoManager {
}
}

// Scan a directory recursively for new video and image files
async scanDirectoryForNewVideos(directoryPath: string): Promise<string[]> {
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 {
Expand Down