feat: implement handlers to store last export location
This commit is contained in:
+60
-42
@@ -822,54 +822,72 @@ 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"] }];
|
||||
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"] }];
|
||||
|
||||
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"],
|
||||
});
|
||||
// Prefer the user's last export folder if it still exists, otherwise fall
|
||||
// back to ~/Downloads. Validation must happen here because the renderer
|
||||
// can't stat the filesystem.
|
||||
let defaultDir = app.getPath("downloads");
|
||||
if (exportFolder) {
|
||||
try {
|
||||
const stats = await fs.stat(exportFolder);
|
||||
if (stats.isDirectory()) {
|
||||
defaultDir = exportFolder;
|
||||
}
|
||||
} catch {
|
||||
// Folder was moved or deleted since the last export; keep Downloads.
|
||||
}
|
||||
}
|
||||
|
||||
if (result.canceled || !result.filePath) {
|
||||
const result = await dialog.showSaveDialog({
|
||||
title: isGif
|
||||
? mainT("dialogs", "fileDialogs.saveGif")
|
||||
: mainT("dialogs", "fileDialogs.saveVideo"),
|
||||
defaultPath: path.join(defaultDir, fileName),
|
||||
filters,
|
||||
properties: ["createDirectory", "showOverwriteConfirmation"],
|
||||
});
|
||||
|
||||
if (result.canceled || !result.filePath) {
|
||||
return {
|
||||
success: false,
|
||||
canceled: true,
|
||||
message: "Export canceled",
|
||||
};
|
||||
}
|
||||
|
||||
// --- FIX: Normalize the path for Windows compatibility ---
|
||||
const normalizedPath = path.normalize(result.filePath);
|
||||
|
||||
// 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,
|
||||
message: "Video exported successfully",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to save exported video:", error);
|
||||
return {
|
||||
success: false,
|
||||
canceled: true,
|
||||
message: "Export canceled",
|
||||
message: "Failed to save exported video",
|
||||
error: String(error),
|
||||
};
|
||||
}
|
||||
|
||||
// --- FIX: Normalize the path for Windows compatibility ---
|
||||
const normalizedPath = path.normalize(result.filePath);
|
||||
|
||||
// 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,
|
||||
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({
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
import { computeFrameStepTime } from "@/lib/frameStep";
|
||||
import type { ProjectMedia } from "@/lib/recordingSession";
|
||||
import { matchesShortcut } from "@/lib/shortcuts";
|
||||
import { loadUserPreferences, saveUserPreferences } from "@/lib/userPreferences";
|
||||
import { loadUserPreferences, parentDirectoryOf, saveUserPreferences } from "@/lib/userPreferences";
|
||||
import { BackgroundLoadError } from "@/lib/wallpaper";
|
||||
import {
|
||||
getAspectRatioValue,
|
||||
@@ -1285,6 +1285,10 @@ export default function VideoEditor() {
|
||||
const handleExportSaved = useCallback(
|
||||
(formatLabel: "GIF" | "Video", filePath: string) => {
|
||||
setExportedFilePath(filePath);
|
||||
const folder = parentDirectoryOf(filePath);
|
||||
if (folder) {
|
||||
saveUserPreferences({ exportFolder: folder });
|
||||
}
|
||||
toast.success(
|
||||
t("export.exportedSuccessfully", {
|
||||
format: formatLabel,
|
||||
@@ -1309,6 +1313,7 @@ export default function VideoEditor() {
|
||||
const saveResult = await window.electronAPI.saveExportedVideo(
|
||||
unsavedExport.arrayBuffer,
|
||||
unsavedExport.fileName,
|
||||
loadUserPreferences().exportFolder ?? undefined,
|
||||
);
|
||||
if (saveResult.canceled) {
|
||||
toast.info("Export canceled");
|
||||
@@ -1410,7 +1415,11 @@ export default function VideoEditor() {
|
||||
}
|
||||
}
|
||||
|
||||
const saveResult = await window.electronAPI.saveExportedVideo(arrayBuffer, fileName);
|
||||
const saveResult = await window.electronAPI.saveExportedVideo(
|
||||
arrayBuffer,
|
||||
fileName,
|
||||
loadUserPreferences().exportFolder ?? undefined,
|
||||
);
|
||||
|
||||
if (saveResult.canceled) {
|
||||
setUnsavedExport({ arrayBuffer, fileName, format: "gif" });
|
||||
@@ -1550,7 +1559,11 @@ export default function VideoEditor() {
|
||||
}
|
||||
}
|
||||
|
||||
const saveResult = await window.electronAPI.saveExportedVideo(arrayBuffer, fileName);
|
||||
const saveResult = await window.electronAPI.saveExportedVideo(
|
||||
arrayBuffer,
|
||||
fileName,
|
||||
loadUserPreferences().exportFolder ?? undefined,
|
||||
);
|
||||
|
||||
if (saveResult.canceled) {
|
||||
setUnsavedExport({ arrayBuffer, fileName, format: "mp4" });
|
||||
|
||||
@@ -23,6 +23,8 @@ export interface UserPreferences {
|
||||
exportQuality: ExportQuality;
|
||||
/** Default export format */
|
||||
exportFormat: ExportFormat;
|
||||
/** Folder used for the most recent successful export, if any */
|
||||
exportFolder: string | null;
|
||||
}
|
||||
|
||||
const DEFAULT_PREFS: UserPreferences = {
|
||||
@@ -30,6 +32,7 @@ const DEFAULT_PREFS: UserPreferences = {
|
||||
aspectRatio: "16:9",
|
||||
exportQuality: "good",
|
||||
exportFormat: "mp4",
|
||||
exportFolder: null,
|
||||
};
|
||||
|
||||
function safeJsonParse(text: string | null): Record<string, unknown> | null {
|
||||
@@ -76,9 +79,24 @@ 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
|
||||
: DEFAULT_PREFS.exportFolder,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the parent directory from a saved file path. Handles both POSIX
|
||||
* and Windows separators since the path comes from the OS save dialog.
|
||||
* Returns null if no separator is found.
|
||||
*/
|
||||
export function parentDirectoryOf(filePath: string): string | null {
|
||||
const lastSep = Math.max(filePath.lastIndexOf("/"), filePath.lastIndexOf("\\"));
|
||||
if (lastSep <= 0) return null;
|
||||
return filePath.slice(0, lastSep);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist user preferences to localStorage.
|
||||
* Only the explicitly provided fields are updated.
|
||||
|
||||
Reference in New Issue
Block a user