diff --git a/src/main/engine/MediaEngine.ts b/src/main/engine/MediaEngine.ts index 2479dfb..30b371c 100644 --- a/src/main/engine/MediaEngine.ts +++ b/src/main/engine/MediaEngine.ts @@ -638,6 +638,96 @@ export class MediaEngine extends EventEmitter { await taskManager.runTask(task); } + + /** + * Regenerate missing thumbnails for all image media. + * Useful for media imported externally without thumbnails. + */ + async regenerateMissingThumbnails(): Promise<{ processed: number; generated: number; failed: number }> { + const result = { processed: 0, generated: 0, failed: 0 }; + + const task: Task<{ processed: number; generated: number; failed: number }> = { + id: uuidv4(), + name: 'Generate missing thumbnails', + execute: async (onProgress) => { + const db = getDatabase().getLocal(); + + onProgress(0, 'Finding images without thumbnails...'); + + // Get all image media for current project + const allMedia = await db + .select() + .from(media) + .where(eq(media.projectId, this.currentProjectId)) + .all(); + + // Filter to images only (not SVG - they don't need thumbnails) + const imageMedia = allMedia.filter( + m => m.mimeType.startsWith('image/') && !m.mimeType.includes('svg') + ); + + if (imageMedia.length === 0) { + onProgress(100, 'No images found'); + return result; + } + + onProgress(5, `Checking ${imageMedia.length} images...`); + + // Find which ones are missing thumbnails + const missingThumbnails: typeof imageMedia = []; + for (const item of imageMedia) { + const thumbnails = await this.getThumbnailPaths(item.id); + // Consider missing if any size is missing + if (!thumbnails.small || !thumbnails.medium || !thumbnails.large) { + missingThumbnails.push(item); + } + } + + if (missingThumbnails.length === 0) { + onProgress(100, 'All thumbnails exist'); + return result; + } + + onProgress(10, `Generating thumbnails for ${missingThumbnails.length} images...`); + + for (let i = 0; i < missingThumbnails.length; i++) { + const item = missingThumbnails[i]; + result.processed++; + + const progress = 10 + (90 * ((i + 1) / missingThumbnails.length)); + onProgress(progress, `Processing ${i + 1}/${missingThumbnails.length}: ${item.originalName}`); + + try { + // Verify the source file exists + await fs.access(item.filePath); + + const thumbnails = await this.generateThumbnails(item.id, item.filePath); + + // Check if thumbnails were actually generated + if (Object.keys(thumbnails).length > 0) { + result.generated++; + } else { + result.failed++; + } + } catch (error) { + console.error(`Failed to generate thumbnails for ${item.id}:`, error); + result.failed++; + } + + // Yield to event loop periodically + if (i % 5 === 0) { + await new Promise(resolve => setImmediate(resolve)); + } + } + + onProgress(100, `Generated ${result.generated} thumbnails, ${result.failed} failed`); + this.emit('thumbnailsRegenerated', result); + return result; + }, + }; + + return await taskManager.runTask(task); + } } // Singleton instance diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts index 26b7e62..ea32814 100644 --- a/src/main/ipc/handlers.ts +++ b/src/main/ipc/handlers.ts @@ -327,6 +327,16 @@ export function registerIpcHandlers(): void { return null; }); + ipcMain.handle('media:regenerateMissingThumbnails', async () => { + const projectEngine = getProjectEngine(); + const project = await projectEngine.getActiveProject(); + const engine = getMediaEngine(); + if (project) { + engine.setProjectContext(project.id); + } + return engine.regenerateMissingThumbnails(); + }); + // ============ Sync Handlers ============ ipcMain.handle('sync:configure', async (_, config: SyncConfig) => { diff --git a/src/main/preload.ts b/src/main/preload.ts index ce72658..a85ca85 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -56,6 +56,7 @@ contextBridge.exposeInMainWorld('electronAPI', { rebuildFromFiles: () => ipcRenderer.invoke('media:rebuildFromFiles'), getThumbnail: (id: string, size?: 'small' | 'medium' | 'large') => ipcRenderer.invoke('media:getThumbnail', id, size), regenerateThumbnails: (id: string) => ipcRenderer.invoke('media:regenerateThumbnails', id), + regenerateMissingThumbnails: () => ipcRenderer.invoke('media:regenerateMissingThumbnails'), }, // Sync diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 2ae7b67..3f37a9e 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -342,6 +342,8 @@ const App: React.FC = () => { // Fire and forget - the handlers return immediately now window.electronAPI?.posts.rebuildFromFiles(); window.electronAPI?.media.rebuildFromFiles(); + // Also regenerate missing thumbnails after media rebuild + window.electronAPI?.media.regenerateMissingThumbnails(); }) || (() => {}) ); diff --git a/src/renderer/components/SettingsView/SettingsView.tsx b/src/renderer/components/SettingsView/SettingsView.tsx index 7aedbf8..ddf6b17 100644 --- a/src/renderer/components/SettingsView/SettingsView.tsx +++ b/src/renderer/components/SettingsView/SettingsView.tsx @@ -939,6 +939,35 @@ export const SettingsView: React.FC = () => { Rebuild Links + + + + ) => Promise; delete: (id: string) => Promise; get: (id: string) => Promise; + getUrl: (id: string) => Promise; + getFilePath: (id: string) => Promise; getAll: () => Promise; rebuildFromFiles: () => Promise; + getThumbnail: (id: string, size?: 'small' | 'medium' | 'large') => Promise; + regenerateThumbnails: (id: string) => Promise | null>; + regenerateMissingThumbnails: () => Promise<{ processed: number; generated: number; failed: number }>; }; sync: { configure: (config: SyncConfig) => Promise;