diff --git a/src/electron/main.ts b/src/electron/main.ts index 60446e7..d3ab980 100644 --- a/src/electron/main.ts +++ b/src/electron/main.ts @@ -201,6 +201,55 @@ function setupIPC() { userData: app.getPath('userData'), isPackaged: app.isPackaged, })); + + // Save rendered file to user-chosen location + ipcMain.handle('file:saveRendered', async (_event, renderUrl: string, defaultName: string) => { + if (!mainWindow) return null; + + // Extract the filename from the URL (e.g. /api/renders/abc123.mp4 → abc123.mp4) + const filename = renderUrl.split('/').pop(); + if (!filename) return null; + + // Determine the source file path in RENDERS_DIR + const rendersDir = process.env.BRADLY_RENDERS_DIR + || path.join(process.cwd(), 'renders'); + const sourcePath = path.join(rendersDir, filename); + + // Verify the source file exists + if (!fs.existsSync(sourcePath)) { + console.error('❌ Rendered file not found:', sourcePath); + return null; + } + + // Determine file extension and filters + const ext = path.extname(filename).slice(1); // 'mp4', 'png', etc. + const filterMap: Record = { + mp4: [{ name: 'Video MP4', extensions: ['mp4'] }], + webm: [{ name: 'Video WebM', extensions: ['webm'] }], + gif: [{ name: 'GIF', extensions: ['gif'] }], + png: [{ name: 'Imagen PNG', extensions: ['png'] }], + jpeg: [{ name: 'Imagen JPEG', extensions: ['jpeg', 'jpg'] }], + }; + + // Show native save dialog + const result = await dialog.showSaveDialog(mainWindow, { + title: 'Guardar exportación', + defaultPath: path.join(app.getPath('downloads'), defaultName), + filters: filterMap[ext] || [{ name: 'Archivo', extensions: [ext] }], + }); + + if (result.canceled || !result.filePath) return null; + + // Copy the rendered file to the chosen location + try { + await fs.promises.copyFile(sourcePath, result.filePath); + console.log(`✅ Saved to: ${result.filePath}`); + return result.filePath; + } catch (err) { + console.error('❌ Failed to save file:', err); + return null; + } + }); } // ═══ Remotion Browser Pre-download ═══ diff --git a/src/electron/preload.ts b/src/electron/preload.ts index 1ac99f7..ca1f04b 100644 --- a/src/electron/preload.ts +++ b/src/electron/preload.ts @@ -24,6 +24,19 @@ const electronAPI = { showOpenDialog: (options: Electron.OpenDialogOptions) => ipcRenderer.invoke('dialog:open', options) as Promise, + // ─── File Operations ─── + + /** + * Save a rendered file to a user-chosen location. + * Shows a native save dialog, then copies the rendered file to the chosen path. + * + * @param renderUrl - The relative URL of the rendered file (e.g. /api/renders/abc.mp4) + * @param defaultName - Suggested filename for the save dialog + * @returns The saved file path, or null if cancelled + */ + saveRenderedFile: (renderUrl: string, defaultName: string) => + ipcRenderer.invoke('file:saveRendered', renderUrl, defaultName) as Promise, + // ─── App Info ─── getAppInfo: () => ipcRenderer.invoke('app:info') as Promise<{ diff --git a/src/hooks/useExportQueue.ts b/src/hooks/useExportQueue.ts index 2949fac..6118a5f 100644 --- a/src/hooks/useExportQueue.ts +++ b/src/hooks/useExportQueue.ts @@ -176,12 +176,29 @@ export function useExportQueue() { }, []); // ─── Download ─── - const downloadJob = useCallback((job: RenderJobClient) => { + const downloadJob = useCallback(async (job: RenderJobClient) => { if (!job.downloadUrl) return; + const defaultName = `export-${job.id.slice(0, 8)}.${job.format}`; + + // In Electron, use native save dialog via IPC + const electronAPI = (window as any).electronAPI; + if (electronAPI?.saveRenderedFile) { + try { + const savedPath = await electronAPI.saveRenderedFile(job.downloadUrl, defaultName); + if (savedPath) { + console.log('✅ Saved to:', savedPath); + } + } catch (err) { + console.error('Save failed:', err); + } + return; + } + + // Web fallback: tag download const a = document.createElement('a'); a.href = job.downloadUrl; - a.download = `export-${job.id.slice(0, 8)}.${job.format}`; + a.download = defaultName; document.body.appendChild(a); a.click(); document.body.removeChild(a);