diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 85d82946..94fd9730 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -76,7 +76,14 @@ interface Window { saveExportedVideo: ( videoData: ArrayBuffer, fileName: string, - ) => Promise<{ success: boolean; path?: string; message?: string; canceled?: boolean }>; + exportFolder?: string, + ) => Promise<{ + success: boolean; + path?: string; + dir?: string; + message?: string; + canceled?: boolean; + }>; openVideoFilePicker: () => Promise<{ success: boolean; path?: string; canceled?: boolean }>; setCurrentVideoPath: (path: string) => Promise<{ success: boolean }>; setCurrentRecordingSession: ( diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 95ed797e..2668970c 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -822,54 +822,60 @@ export function registerIpcHandlers( * @returns Object with success status, optional file path, and error details. */ - ipcMain.handle("save-exported-video", async (_, videoData: ArrayBuffer, fileName: string) => { - try { - // Determine file type from extension - const isGif = fileName.toLowerCase().endsWith(".gif"); - const filters = isGif - ? [{ name: mainT("dialogs", "fileDialogs.gifImage"), extensions: ["gif"] }] - : [{ name: mainT("dialogs", "fileDialogs.mp4Video"), extensions: ["mp4"] }]; - - const result = await dialog.showSaveDialog({ - title: isGif - ? mainT("dialogs", "fileDialogs.saveGif") - : mainT("dialogs", "fileDialogs.saveVideo"), - defaultPath: path.join(app.getPath("downloads"), fileName), - filters, - properties: ["createDirectory", "showOverwriteConfirmation"], - }); + ipcMain.handle( + "save-exported-video", + async (_, videoData: ArrayBuffer, fileName: string, exportFolder?: string) => { + try { + // Determine file type from extension + const isGif = fileName.toLowerCase().endsWith(".gif"); + const filters = isGif + ? [{ name: mainT("dialogs", "fileDialogs.gifImage"), extensions: ["gif"] }] + : [{ name: mainT("dialogs", "fileDialogs.mp4Video"), extensions: ["mp4"] }]; - if (result.canceled || !result.filePath) { - return { - success: false, - canceled: true, - message: "Export canceled", - }; - } + const baseFolder = exportFolder ?? app.getPath("downloads"); - // --- FIX: Normalize the path for Windows compatibility --- - const normalizedPath = path.normalize(result.filePath); + const result = await dialog.showSaveDialog({ + title: isGif + ? mainT("dialogs", "fileDialogs.saveGif") + : mainT("dialogs", "fileDialogs.saveVideo"), + defaultPath: path.join(baseFolder, fileName), + filters, + properties: ["createDirectory", "showOverwriteConfirmation"], + }); - // Ensure the parent directory exists (Windows may fail if the folder is missing) - await fs.mkdir(path.dirname(normalizedPath), { recursive: true }); - // --- END FIX --- + if (result.canceled || !result.filePath) { + return { + success: false, + canceled: true, + message: "Export canceled", + }; + } - await fs.writeFile(normalizedPath, Buffer.from(videoData)); + // --- FIX: Normalize the path for Windows compatibility --- + const normalizedPath = path.normalize(result.filePath); - return { - success: true, - path: normalizedPath, - message: "Video exported successfully", - }; - } catch (error) { - console.error("Failed to save exported video:", error); - return { - success: false, - message: "Failed to save exported video", - error: String(error), - }; - } - }); + // Ensure the parent directory exists (Windows may fail if the folder is missing) + await fs.mkdir(path.dirname(normalizedPath), { recursive: true }); + // --- END FIX --- + + await fs.writeFile(normalizedPath, Buffer.from(videoData)); + + return { + success: true, + path: normalizedPath, + dir: path.dirname(normalizedPath), + message: "Video exported successfully", + }; + } catch (error) { + console.error("Failed to save exported video:", error); + return { + success: false, + message: "Failed to save exported video", + error: String(error), + }; + } + }, + ); ipcMain.handle("open-video-file-picker", async () => { try { const result = await dialog.showOpenDialog({ diff --git a/electron/preload.ts b/electron/preload.ts index 46e16f04..ec221b06 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -68,8 +68,8 @@ contextBridge.exposeInMainWorld("electronAPI", { openExternalUrl: (url: string) => { return ipcRenderer.invoke("open-external-url", url); }, - saveExportedVideo: (videoData: ArrayBuffer, fileName: string) => { - return ipcRenderer.invoke("save-exported-video", videoData, fileName); + saveExportedVideo: (videoData: ArrayBuffer, fileName: string, exportFolder?: string) => { + return ipcRenderer.invoke("save-exported-video", videoData, fileName, exportFolder); }, openVideoFilePicker: () => { return ipcRenderer.invoke("open-video-file-picker"); diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 7adc558e..0cf6f250 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -133,6 +133,7 @@ export default function VideoEditor() { const [showNewRecordingDialog, setShowNewRecordingDialog] = useState(false); const [exportQuality, setExportQuality] = useState("good"); const [exportFormat, setExportFormat] = useState("mp4"); + const [exportFolder, setExportFolder] = useState(undefined); const [gifFrameRate, setGifFrameRate] = useState(15); const [gifLoop, setGifLoop] = useState(true); const [gifSizePreset, setGifSizePreset] = useState("medium"); @@ -409,14 +410,15 @@ export default function VideoEditor() { }); setExportQuality(prefs.exportQuality); setExportFormat(prefs.exportFormat); + setExportFolder(prefs.exportFolder); setPrefsHydrated(true); }, [updateState]); // Auto-save user preferences when settings change useEffect(() => { if (!prefsHydrated) return; - saveUserPreferences({ padding, aspectRatio, exportQuality, exportFormat }); - }, [prefsHydrated, padding, aspectRatio, exportQuality, exportFormat]); + saveUserPreferences({ padding, aspectRatio, exportQuality, exportFormat, exportFolder }); + }, [prefsHydrated, padding, aspectRatio, exportQuality, exportFormat, exportFolder]); const saveProject = useCallback( async (forceSaveAs: boolean) => { @@ -1309,10 +1311,12 @@ export default function VideoEditor() { const saveResult = await window.electronAPI.saveExportedVideo( unsavedExport.arrayBuffer, unsavedExport.fileName, + exportFolder, ); if (saveResult.canceled) { toast.info("Export canceled"); } else if (saveResult.success && saveResult.path) { + if (saveResult.dir) setExportFolder(saveResult.dir); setUnsavedExport(null); handleExportSaved(unsavedExport.format === "gif" ? "GIF" : "Video", saveResult.path); } else { @@ -1322,7 +1326,7 @@ export default function VideoEditor() { console.error("Error saving unsaved export:", error); toast.error("Failed to save exported video"); } - }, [unsavedExport, handleExportSaved]); + }, [unsavedExport, exportFolder, handleExportSaved]); const handleExport = useCallback( async (settings: ExportSettings) => { @@ -1410,12 +1414,17 @@ export default function VideoEditor() { } } - const saveResult = await window.electronAPI.saveExportedVideo(arrayBuffer, fileName); + const saveResult = await window.electronAPI.saveExportedVideo( + arrayBuffer, + fileName, + exportFolder, + ); if (saveResult.canceled) { setUnsavedExport({ arrayBuffer, fileName, format: "gif" }); toast.info("Export canceled"); } else if (saveResult.success && saveResult.path) { + if (saveResult.dir) setExportFolder(saveResult.dir); setUnsavedExport(null); handleExportSaved("GIF", saveResult.path); } else { @@ -1550,12 +1559,17 @@ export default function VideoEditor() { } } - const saveResult = await window.electronAPI.saveExportedVideo(arrayBuffer, fileName); + const saveResult = await window.electronAPI.saveExportedVideo( + arrayBuffer, + fileName, + exportFolder, + ); if (saveResult.canceled) { setUnsavedExport({ arrayBuffer, fileName, format: "mp4" }); toast.info("Export canceled"); } else if (saveResult.success && saveResult.path) { + if (saveResult.dir) setExportFolder(saveResult.dir); setUnsavedExport(null); handleExportSaved("Video", saveResult.path); } else { @@ -1612,6 +1626,7 @@ export default function VideoEditor() { webcamSizePreset, webcamPosition, exportQuality, + exportFolder, handleExportSaved, cursorTelemetry, t, diff --git a/src/lib/userPreferences.ts b/src/lib/userPreferences.ts index e0607880..26bec7b2 100644 --- a/src/lib/userPreferences.ts +++ b/src/lib/userPreferences.ts @@ -23,6 +23,8 @@ export interface UserPreferences { exportQuality: ExportQuality; /** Default export format */ exportFormat: ExportFormat; + /** Last folder used when saving an export */ + exportFolder?: string; } const DEFAULT_PREFS: UserPreferences = { @@ -76,6 +78,10 @@ export function loadUserPreferences(): UserPreferences { raw.exportFormat === "gif" || raw.exportFormat === "mp4" ? (raw.exportFormat as ExportFormat) : DEFAULT_PREFS.exportFormat, + exportFolder: + typeof raw.exportFolder === "string" && raw.exportFolder.length > 0 + ? raw.exportFolder + : undefined, }; }