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:
@@ -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<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 ═══
|
||||
|
||||
@@ -24,6 +24,19 @@ const electronAPI = {
|
||||
showOpenDialog: (options: Electron.OpenDialogOptions) =>
|
||||
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 ───
|
||||
getAppInfo: () =>
|
||||
ipcRenderer.invoke('app:info') as Promise<{
|
||||
|
||||
@@ -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: <a> 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);
|
||||
|
||||
Reference in New Issue
Block a user