feat: hooked thumbnail generation to buttons
This commit is contained in:
@@ -638,6 +638,96 @@ export class MediaEngine extends EventEmitter {
|
|||||||
|
|
||||||
await taskManager.runTask(task);
|
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
|
// Singleton instance
|
||||||
|
|||||||
@@ -327,6 +327,16 @@ export function registerIpcHandlers(): void {
|
|||||||
return null;
|
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 ============
|
// ============ Sync Handlers ============
|
||||||
|
|
||||||
ipcMain.handle('sync:configure', async (_, config: SyncConfig) => {
|
ipcMain.handle('sync:configure', async (_, config: SyncConfig) => {
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
rebuildFromFiles: () => ipcRenderer.invoke('media:rebuildFromFiles'),
|
rebuildFromFiles: () => ipcRenderer.invoke('media:rebuildFromFiles'),
|
||||||
getThumbnail: (id: string, size?: 'small' | 'medium' | 'large') => ipcRenderer.invoke('media:getThumbnail', id, size),
|
getThumbnail: (id: string, size?: 'small' | 'medium' | 'large') => ipcRenderer.invoke('media:getThumbnail', id, size),
|
||||||
regenerateThumbnails: (id: string) => ipcRenderer.invoke('media:regenerateThumbnails', id),
|
regenerateThumbnails: (id: string) => ipcRenderer.invoke('media:regenerateThumbnails', id),
|
||||||
|
regenerateMissingThumbnails: () => ipcRenderer.invoke('media:regenerateMissingThumbnails'),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Sync
|
// Sync
|
||||||
|
|||||||
@@ -342,6 +342,8 @@ const App: React.FC = () => {
|
|||||||
// Fire and forget - the handlers return immediately now
|
// Fire and forget - the handlers return immediately now
|
||||||
window.electronAPI?.posts.rebuildFromFiles();
|
window.electronAPI?.posts.rebuildFromFiles();
|
||||||
window.electronAPI?.media.rebuildFromFiles();
|
window.electronAPI?.media.rebuildFromFiles();
|
||||||
|
// Also regenerate missing thumbnails after media rebuild
|
||||||
|
window.electronAPI?.media.regenerateMissingThumbnails();
|
||||||
}) || (() => {})
|
}) || (() => {})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -939,6 +939,35 @@ export const SettingsView: React.FC = () => {
|
|||||||
Rebuild Links
|
Rebuild Links
|
||||||
</button>
|
</button>
|
||||||
</SettingRow>
|
</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>
|
||||||
|
|
||||||
<SettingSection
|
<SettingSection
|
||||||
|
|||||||
5
src/renderer/types/electron.d.ts
vendored
5
src/renderer/types/electron.d.ts
vendored
@@ -272,8 +272,13 @@ export interface ElectronAPI {
|
|||||||
update: (id: string, data: Partial<MediaData>) => Promise<MediaData | null>;
|
update: (id: string, data: Partial<MediaData>) => Promise<MediaData | null>;
|
||||||
delete: (id: string) => Promise<boolean>;
|
delete: (id: string) => Promise<boolean>;
|
||||||
get: (id: string) => Promise<MediaData | null>;
|
get: (id: string) => Promise<MediaData | null>;
|
||||||
|
getUrl: (id: string) => Promise<string | null>;
|
||||||
|
getFilePath: (id: string) => Promise<string | null>;
|
||||||
getAll: () => Promise<MediaData[]>;
|
getAll: () => Promise<MediaData[]>;
|
||||||
rebuildFromFiles: () => Promise<void>;
|
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: {
|
sync: {
|
||||||
configure: (config: SyncConfig) => Promise<void>;
|
configure: (config: SyncConfig) => Promise<void>;
|
||||||
|
|||||||
Reference in New Issue
Block a user