feat: hooked thumbnail generation to buttons

This commit is contained in:
2026-02-11 22:07:32 +01:00
parent adadb7db54
commit 8c82cf5b29
6 changed files with 137 additions and 0 deletions

View File

@@ -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

View File

@@ -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) => {

View File

@@ -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

View File

@@ -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();
}) || (() => {})
);

View File

@@ -939,6 +939,35 @@ export const SettingsView: React.FC = () => {
Rebuild Links
</button>
</SettingRow>
<SettingRow
id="regenerate-thumbnails"
label="Regenerate Thumbnails"
description="Generate missing thumbnails for all images. Useful after importing media externally."
>
<button
className="secondary"
onClick={async () => {
showToast.loading('Generating thumbnails...');
try {
const result = await window.electronAPI?.media.regenerateMissingThumbnails();
showToast.dismiss();
if (result && result.generated > 0) {
showToast.success(`Generated ${result.generated} thumbnails`);
} else if (result && result.processed === 0) {
showToast.success('All thumbnails already exist');
} else {
showToast.success('Thumbnail generation complete');
}
} catch {
showToast.dismiss();
showToast.error('Failed to generate thumbnails');
}
}}
>
Generate Thumbnails
</button>
</SettingRow>
</SettingSection>
<SettingSection

View File

@@ -272,8 +272,13 @@ export interface ElectronAPI {
update: (id: string, data: Partial<MediaData>) => Promise<MediaData | null>;
delete: (id: string) => Promise<boolean>;
get: (id: string) => Promise<MediaData | null>;
getUrl: (id: string) => Promise<string | null>;
getFilePath: (id: string) => Promise<string | null>;
getAll: () => Promise<MediaData[]>;
rebuildFromFiles: () => Promise<void>;
getThumbnail: (id: string, size?: 'small' | 'medium' | 'large') => Promise<string | null>;
regenerateThumbnails: (id: string) => Promise<Record<string, string> | null>;
regenerateMissingThumbnails: () => Promise<{ processed: number; generated: number; failed: number }>;
};
sync: {
configure: (config: SyncConfig) => Promise<void>;