From 719fc4f75b4a561ebd98f779d6b8cd6e5d8635bb Mon Sep 17 00:00:00 2001
From: webadderall <131426131+webadderall@users.noreply.github.com>
Date: Sat, 14 Mar 2026 20:17:32 +1100
Subject: [PATCH] feat(export): allow re-saving after save dialog cancel (#181)
---
src/components/video-editor/ExportDialog.tsx | 12 +++
src/components/video-editor/VideoEditor.tsx | 104 ++++++++++++++-----
2 files changed, 91 insertions(+), 25 deletions(-)
diff --git a/src/components/video-editor/ExportDialog.tsx b/src/components/video-editor/ExportDialog.tsx
index 350004d4..2ba03797 100644
--- a/src/components/video-editor/ExportDialog.tsx
+++ b/src/components/video-editor/ExportDialog.tsx
@@ -12,6 +12,8 @@ interface ExportDialogProps {
isExporting: boolean;
error: string | null;
onCancel?: () => void;
+ onRetrySave?: () => void;
+ canRetrySave?: boolean;
exportFormat?: 'mp4' | 'gif';
exportedFilePath?: string;
}
@@ -23,6 +25,8 @@ export function ExportDialog({
isExporting,
error,
onCancel,
+ onRetrySave,
+ canRetrySave = false,
exportFormat = 'mp4',
exportedFilePath, // Add this line
}: ExportDialogProps) {
@@ -173,6 +177,14 @@ export function ExportDialog({
{error}
+ {!isExporting && canRetrySave && onRetrySave && (
+
+ )}
)}
diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx
index cc930fe1..a41298e5 100644
--- a/src/components/video-editor/VideoEditor.tsx
+++ b/src/components/video-editor/VideoEditor.tsx
@@ -65,6 +65,11 @@ type EditorHistorySnapshot = {
selectedAnnotationId: string | null;
};
+type PendingExportSave = {
+ fileName: string;
+ arrayBuffer: ArrayBuffer;
+};
+
export default function VideoEditor() {
const [videoPath, setVideoPath] = useState(null);
const [videoSourcePath, setVideoSourcePath] = useState(null);
@@ -107,6 +112,7 @@ export default function VideoEditor() {
const [gifLoop, setGifLoop] = useState(true);
const [gifSizePreset, setGifSizePreset] = useState('medium');
const [exportedFilePath, setExportedFilePath] = useState(undefined);
+ const [hasPendingExportSave, setHasPendingExportSave] = useState(false);
const [lastSavedSnapshot, setLastSavedSnapshot] = useState(null);
const videoPlaybackRef = useRef(null);
@@ -123,6 +129,7 @@ export default function VideoEditor() {
const historyFutureRef = useRef([]);
const historyCurrentRef = useRef(null);
const applyingHistoryRef = useRef(false);
+ const pendingExportSaveRef = useRef(null);
const cloneSnapshot = useCallback((snapshot: EditorHistorySnapshot): EditorHistorySnapshot => {
return {
@@ -1103,6 +1110,25 @@ export default function VideoEditor() {
}
}, [selectedSpeedId, speedRegions]);
+ const showExportSuccessToast = useCallback((filePath: string) => {
+ toast.success(`Exported successfully to ${filePath}`, {
+ action: {
+ label: 'Show in Folder',
+ onClick: async () => {
+ try {
+ const result = await window.electronAPI.revealInFolder(filePath);
+ if (!result.success) {
+ const errorMessage = result.error || result.message || 'Failed to reveal item in folder.';
+ toast.error(errorMessage);
+ }
+ } catch (err) {
+ toast.error(`Error revealing in folder: ${String(err)}`);
+ }
+ }
+ }
+ });
+ }, []);
+
const handleExport = useCallback(async (settings: ExportSettings) => {
if (!videoPath) {
toast.error('No video loaded');
@@ -1118,6 +1144,10 @@ export default function VideoEditor() {
setIsExporting(true);
setExportProgress(null);
setExportError(null);
+ pendingExportSaveRef.current = null;
+ setHasPendingExportSave(false);
+
+ let keepExportDialogOpen = false;
try {
const wasPlaying = isPlaying;
@@ -1183,7 +1213,11 @@ export default function VideoEditor() {
const saveResult = await window.electronAPI.saveExportedVideo(arrayBuffer, fileName);
if (saveResult.canceled) {
- toast.info('Export cancelled');
+ pendingExportSaveRef.current = { arrayBuffer, fileName };
+ setHasPendingExportSave(true);
+ setExportError('Save dialog canceled. Click Save Again to save without re-rendering.');
+ toast.info('Save canceled. You can save again without re-exporting.');
+ keepExportDialogOpen = true;
} else if (saveResult.success && saveResult.path) {
showExportSuccessToast(saveResult.path);
setExportedFilePath(saveResult.path);
@@ -1317,7 +1351,11 @@ export default function VideoEditor() {
const saveResult = await window.electronAPI.saveExportedVideo(arrayBuffer, fileName);
if (saveResult.canceled) {
- toast.info('Export cancelled');
+ pendingExportSaveRef.current = { arrayBuffer, fileName };
+ setHasPendingExportSave(true);
+ setExportError('Save dialog canceled. Click Save Again to save without re-rendering.');
+ toast.info('Save canceled. You can save again without re-exporting.');
+ keepExportDialogOpen = true;
} else if (saveResult.success && saveResult.path) {
showExportSuccessToast(saveResult.path);
setExportedFilePath(saveResult.path);
@@ -1348,12 +1386,10 @@ export default function VideoEditor() {
}
setIsExporting(false);
exporterRef.current = null;
- // Reset dialog state to ensure it can be opened again on next export
- // This fixes the bug where second export doesn't show save dialog
- setShowExportDialog(false);
+ setShowExportDialog(keepExportDialogOpen);
setExportProgress(null);
}
- }, [videoPath, wallpaper, zoomRegions, trimRegions, speedRegions, shadowIntensity, backgroundBlur, zoomMotionBlur, connectZooms, showCursor, normalizedCursorTelemetry, cursorSize, cursorSmoothing, cursorMotionBlur, cursorClickBounce, borderRadius, padding, cropRegion, annotationRegions, isPlaying, aspectRatio, exportQuality]);
+ }, [videoPath, wallpaper, zoomRegions, trimRegions, speedRegions, shadowIntensity, backgroundBlur, zoomMotionBlur, connectZooms, showCursor, normalizedCursorTelemetry, cursorSize, cursorSmoothing, cursorMotionBlur, cursorClickBounce, borderRadius, padding, cropRegion, annotationRegions, isPlaying, aspectRatio, exportQuality, showExportSuccessToast]);
const handleOpenExportDialog = useCallback(() => {
if (!videoPath) {
@@ -1361,6 +1397,12 @@ export default function VideoEditor() {
return;
}
+ if (hasPendingExportSave) {
+ setShowExportDialog(true);
+ setExportError('Save dialog canceled. Click Save Again to save without re-rendering.');
+ return;
+ }
+
const video = videoPlaybackRef.current?.video;
if (!video) {
toast.error('Video not ready');
@@ -1389,7 +1431,7 @@ export default function VideoEditor() {
// Start export immediately
handleExport(settings);
- }, [videoPath, exportFormat, exportQuality, gifFrameRate, gifLoop, gifSizePreset, handleExport]);
+ }, [videoPath, hasPendingExportSave, exportFormat, exportQuality, gifFrameRate, gifLoop, gifSizePreset, handleExport]);
const handleCancelExport = useCallback(() => {
if (exporterRef.current) {
@@ -1408,24 +1450,34 @@ export default function VideoEditor() {
setExportedFilePath(undefined);
}, []);
- const showExportSuccessToast = useCallback((filePath: string) => {
- toast.success(`Exported successfully to ${filePath}`, {
- action: {
- label: 'Show in Folder',
- onClick: async () => {
- try {
- const result = await window.electronAPI.revealInFolder(filePath);
- if (!result.success) {
- const errorMessage = result.error || result.message || 'Failed to reveal item in folder.';
- toast.error(errorMessage);
- }
- } catch (err) {
- toast.error(`Error revealing in folder: ${String(err)}`);
- }
- }
- }
- });
- }, []);
+ const handleRetrySaveExport = useCallback(async () => {
+ const pendingSave = pendingExportSaveRef.current;
+ if (!pendingSave) {
+ return;
+ }
+
+ const saveResult = await window.electronAPI.saveExportedVideo(pendingSave.arrayBuffer, pendingSave.fileName);
+
+ if (saveResult.canceled) {
+ setExportError('Save dialog canceled. Click Save Again to save without re-rendering.');
+ toast.info('Save canceled. You can try again.');
+ return;
+ }
+
+ if (saveResult.success && saveResult.path) {
+ pendingExportSaveRef.current = null;
+ setHasPendingExportSave(false);
+ setExportError(null);
+ setExportedFilePath(saveResult.path);
+ showExportSuccessToast(saveResult.path);
+ setShowExportDialog(false);
+ return;
+ }
+
+ const errorMessage = saveResult.message || 'Failed to save video';
+ setExportError(errorMessage);
+ toast.error(errorMessage);
+ }, [showExportSuccessToast]);
const openRecordingsFolder = useCallback(async () => {
try {
@@ -1671,6 +1723,8 @@ export default function VideoEditor() {
isExporting={isExporting}
error={exportError}
onCancel={handleCancelExport}
+ onRetrySave={handleRetrySaveExport}
+ canRetrySave={hasPendingExportSave}
exportFormat={exportFormat}
exportedFilePath={exportedFilePath}
/>