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;