loc first and then export processing
This commit is contained in:
Vendored
+10
-2
@@ -79,8 +79,7 @@ interface Window {
|
||||
}>;
|
||||
onStopRecordingFromTray: (callback: () => void) => () => void;
|
||||
openExternalUrl: (url: string) => Promise<{ success: boolean; error?: string }>;
|
||||
saveExportedVideo: (
|
||||
videoData: ArrayBuffer,
|
||||
pickExportSavePath: (
|
||||
fileName: string,
|
||||
exportFolder?: string,
|
||||
) => Promise<{
|
||||
@@ -90,6 +89,15 @@ interface Window {
|
||||
canceled?: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
writeExportToPath: (
|
||||
videoData: ArrayBuffer,
|
||||
filePath: string,
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
openVideoFilePicker: () => Promise<{ success: boolean; path?: string; canceled?: boolean }>;
|
||||
setCurrentVideoPath: (path: string) => Promise<{ success: boolean }>;
|
||||
setCurrentRecordingSession: (
|
||||
|
||||
+78
-71
@@ -986,81 +986,88 @@ export function registerIpcHandlers(
|
||||
* @returns Object with success status, optional file path, and error details.
|
||||
*/
|
||||
|
||||
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"] }];
|
||||
ipcMain.handle("pick-export-save-path", async (_, fileName: string, exportFolder?: string) => {
|
||||
try {
|
||||
const isGif = fileName.toLowerCase().endsWith(".gif");
|
||||
const filters = isGif
|
||||
? [{ name: mainT("dialogs", "fileDialogs.gifImage"), extensions: ["gif"] }]
|
||||
: [{ name: mainT("dialogs", "fileDialogs.mp4Video"), extensions: ["mp4"] }];
|
||||
|
||||
// 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 (err) {
|
||||
// Stat can fail because the folder was moved/deleted (expected) or
|
||||
// because of a permission error (worth surfacing). Either way we
|
||||
// fall back to Downloads, but log so debugging isn't blind.
|
||||
console.warn(
|
||||
`Could not access remembered export folder "${exportFolder}", falling back to Downloads:`,
|
||||
err,
|
||||
);
|
||||
// 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 (err) {
|
||||
console.warn(
|
||||
`Could not access remembered export folder "${exportFolder}", falling back to Downloads:`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
const dialogOptions = buildDialogOptions(
|
||||
{
|
||||
title: isGif
|
||||
? mainT("dialogs", "fileDialogs.saveGif")
|
||||
: mainT("dialogs", "fileDialogs.saveVideo"),
|
||||
defaultPath: path.join(defaultDir, fileName),
|
||||
filters,
|
||||
properties: ["createDirectory", "showOverwriteConfirmation"],
|
||||
},
|
||||
getMainWindow(),
|
||||
);
|
||||
const result = await dialog.showSaveDialog(dialogOptions);
|
||||
|
||||
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,
|
||||
message: "Failed to save exported video",
|
||||
error: String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
const dialogOptions = buildDialogOptions(
|
||||
{
|
||||
title: isGif
|
||||
? mainT("dialogs", "fileDialogs.saveGif")
|
||||
: mainT("dialogs", "fileDialogs.saveVideo"),
|
||||
defaultPath: path.join(defaultDir, fileName),
|
||||
filters,
|
||||
properties: ["createDirectory", "showOverwriteConfirmation"],
|
||||
},
|
||||
getMainWindow(),
|
||||
);
|
||||
const result = await dialog.showSaveDialog(dialogOptions);
|
||||
|
||||
if (result.canceled || !result.filePath) {
|
||||
return { success: false, canceled: true, message: "Export canceled" };
|
||||
}
|
||||
|
||||
return { success: true, path: path.normalize(result.filePath) };
|
||||
} catch (error) {
|
||||
console.error("Failed to show save dialog:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to show save dialog",
|
||||
error: String(error),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("write-export-to-path", async (_, videoData: ArrayBuffer, filePath: string) => {
|
||||
try {
|
||||
// Sanity-check the path. The renderer is trusted (contextIsolation is on),
|
||||
// but a stale state bug shouldn't be able to clobber arbitrary files.
|
||||
if (typeof filePath !== "string" || !path.isAbsolute(filePath)) {
|
||||
return { success: false, message: "Invalid path" };
|
||||
}
|
||||
const lower = filePath.toLowerCase();
|
||||
if (!lower.endsWith(".mp4") && !lower.endsWith(".gif")) {
|
||||
return { success: false, message: "Invalid file type" };
|
||||
}
|
||||
|
||||
const normalizedPath = path.normalize(filePath);
|
||||
await fs.mkdir(path.dirname(normalizedPath), { recursive: true });
|
||||
await fs.writeFile(normalizedPath, Buffer.from(videoData));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
path: normalizedPath,
|
||||
message: "Video exported successfully",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to write exported video:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to save exported video",
|
||||
error: String(error),
|
||||
};
|
||||
}
|
||||
});
|
||||
ipcMain.handle("open-video-file-picker", async () => {
|
||||
try {
|
||||
const dialogOptions = buildDialogOptions(
|
||||
|
||||
+5
-2
@@ -71,8 +71,11 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
openExternalUrl: (url: string) => {
|
||||
return ipcRenderer.invoke("open-external-url", url);
|
||||
},
|
||||
saveExportedVideo: (videoData: ArrayBuffer, fileName: string, exportFolder?: string) => {
|
||||
return ipcRenderer.invoke("save-exported-video", videoData, fileName, exportFolder);
|
||||
pickExportSavePath: (fileName: string, exportFolder?: string) => {
|
||||
return ipcRenderer.invoke("pick-export-save-path", fileName, exportFolder);
|
||||
},
|
||||
writeExportToPath: (videoData: ArrayBuffer, filePath: string) => {
|
||||
return ipcRenderer.invoke("write-export-to-path", videoData, filePath);
|
||||
},
|
||||
openVideoFilePicker: () => {
|
||||
return ipcRenderer.invoke("open-video-file-picker");
|
||||
|
||||
@@ -1395,14 +1395,19 @@ export default function VideoEditor() {
|
||||
const handleSaveUnsavedExport = useCallback(async () => {
|
||||
if (!unsavedExport) return;
|
||||
try {
|
||||
const saveResult = await window.electronAPI.saveExportedVideo(
|
||||
unsavedExport.arrayBuffer,
|
||||
const pickResult = await window.electronAPI.pickExportSavePath(
|
||||
unsavedExport.fileName,
|
||||
getExportFolder(),
|
||||
);
|
||||
if (saveResult.canceled) {
|
||||
if (pickResult.canceled || !pickResult.success || !pickResult.path) {
|
||||
toast.info("Export canceled");
|
||||
} else if (saveResult.success && saveResult.path) {
|
||||
return;
|
||||
}
|
||||
const saveResult = await window.electronAPI.writeExportToPath(
|
||||
unsavedExport.arrayBuffer,
|
||||
pickResult.path,
|
||||
);
|
||||
if (saveResult.success && saveResult.path) {
|
||||
setUnsavedExport(null);
|
||||
handleExportSaved(unsavedExport.format === "gif" ? "GIF" : "Video", saveResult.path);
|
||||
} else {
|
||||
@@ -1427,6 +1432,21 @@ export default function VideoEditor() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ask the user where to save BEFORE starting the export. This avoids the
|
||||
// post-export save dialog getting hidden behind other windows after a
|
||||
// long-running export.
|
||||
const isGifFormat = settings.format === "gif";
|
||||
const targetFileName = `export-${Date.now()}.${isGifFormat ? "gif" : "mp4"}`;
|
||||
const pickResult = await window.electronAPI.pickExportSavePath(
|
||||
targetFileName,
|
||||
getExportFolder(),
|
||||
);
|
||||
if (pickResult.canceled || !pickResult.success || !pickResult.path) {
|
||||
setShowExportDialog(false);
|
||||
return;
|
||||
}
|
||||
const targetPath = pickResult.path;
|
||||
|
||||
setIsExporting(true);
|
||||
setExportProgress(null);
|
||||
setExportError(null);
|
||||
@@ -1493,8 +1513,6 @@ export default function VideoEditor() {
|
||||
|
||||
if (result.success && result.blob) {
|
||||
const arrayBuffer = await result.blob.arrayBuffer();
|
||||
const timestamp = Date.now();
|
||||
const fileName = `export-${timestamp}.gif`;
|
||||
|
||||
if (result.warnings) {
|
||||
for (const warning of result.warnings) {
|
||||
@@ -1502,19 +1520,13 @@ export default function VideoEditor() {
|
||||
}
|
||||
}
|
||||
|
||||
const saveResult = await window.electronAPI.saveExportedVideo(
|
||||
arrayBuffer,
|
||||
fileName,
|
||||
getExportFolder(),
|
||||
);
|
||||
const saveResult = await window.electronAPI.writeExportToPath(arrayBuffer, targetPath);
|
||||
|
||||
if (saveResult.canceled) {
|
||||
setUnsavedExport({ arrayBuffer, fileName, format: "gif" });
|
||||
toast.info("Export canceled");
|
||||
} else if (saveResult.success && saveResult.path) {
|
||||
if (saveResult.success && saveResult.path) {
|
||||
setUnsavedExport(null);
|
||||
handleExportSaved("GIF", saveResult.path);
|
||||
} else {
|
||||
setUnsavedExport({ arrayBuffer, fileName: targetFileName, format: "gif" });
|
||||
setExportError(saveResult.message || "Failed to save GIF");
|
||||
toast.error(saveResult.message || "Failed to save GIF");
|
||||
}
|
||||
@@ -1642,8 +1654,6 @@ export default function VideoEditor() {
|
||||
|
||||
if (result.success && result.blob) {
|
||||
const arrayBuffer = await result.blob.arrayBuffer();
|
||||
const timestamp = Date.now();
|
||||
const fileName = `export-${timestamp}.mp4`;
|
||||
|
||||
if (result.warnings) {
|
||||
for (const warning of result.warnings) {
|
||||
@@ -1651,19 +1661,13 @@ export default function VideoEditor() {
|
||||
}
|
||||
}
|
||||
|
||||
const saveResult = await window.electronAPI.saveExportedVideo(
|
||||
arrayBuffer,
|
||||
fileName,
|
||||
getExportFolder(),
|
||||
);
|
||||
const saveResult = await window.electronAPI.writeExportToPath(arrayBuffer, targetPath);
|
||||
|
||||
if (saveResult.canceled) {
|
||||
setUnsavedExport({ arrayBuffer, fileName, format: "mp4" });
|
||||
toast.info("Export canceled");
|
||||
} else if (saveResult.success && saveResult.path) {
|
||||
if (saveResult.success && saveResult.path) {
|
||||
setUnsavedExport(null);
|
||||
handleExportSaved("Video", saveResult.path);
|
||||
} else {
|
||||
setUnsavedExport({ arrayBuffer, fileName: targetFileName, format: "mp4" });
|
||||
setExportError(saveResult.message || "Failed to save video");
|
||||
toast.error(saveResult.message || "Failed to save video");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user