fix: Electron download via native save dialog

- Add file:saveRendered IPC handler in main process
- Copies rendered file from internal renders dir to user-chosen path
- Update preload with saveRenderedFile bridge method
- Update useExportQueue downloadJob: detect Electron → native save dialog
- Web mode fallback preserved (<a> tag download)
This commit is contained in:
2026-06-02 04:33:57 -05:00
parent fbdbd7e05c
commit 25587ab07f
3 changed files with 81 additions and 2 deletions
+49
View File
@@ -201,6 +201,55 @@ function setupIPC() {
userData: app.getPath('userData'), userData: app.getPath('userData'),
isPackaged: app.isPackaged, 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<string, { name: string; extensions: string[] }[]> = {
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 ═══ // ═══ Remotion Browser Pre-download ═══
+13
View File
@@ -24,6 +24,19 @@ const electronAPI = {
showOpenDialog: (options: Electron.OpenDialogOptions) => showOpenDialog: (options: Electron.OpenDialogOptions) =>
ipcRenderer.invoke('dialog:open', options) as Promise<Electron.OpenDialogReturnValue>, ipcRenderer.invoke('dialog:open', options) as Promise<Electron.OpenDialogReturnValue>,
// ─── 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<string | null>,
// ─── App Info ─── // ─── App Info ───
getAppInfo: () => getAppInfo: () =>
ipcRenderer.invoke('app:info') as Promise<{ ipcRenderer.invoke('app:info') as Promise<{
+19 -2
View File
@@ -176,12 +176,29 @@ export function useExportQueue() {
}, []); }, []);
// ─── Download ─── // ─── Download ───
const downloadJob = useCallback((job: RenderJobClient) => { const downloadJob = useCallback(async (job: RenderJobClient) => {
if (!job.downloadUrl) return; 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: <a> tag download
const a = document.createElement('a'); const a = document.createElement('a');
a.href = job.downloadUrl; a.href = job.downloadUrl;
a.download = `export-${job.id.slice(0, 8)}.${job.format}`; a.download = defaultName;
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);