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'),
|
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 ═══
|
||||||
|
|||||||
@@ -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<{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user