feat: add reveal in folder option after export

- Added electron IPC handler 'reveal-in-folder' to show exported file in finder
- Created toast notification with clickable action to reveal exported video
- Added Show in Folder button in export success dialog
- Implemented proper state management for exported file path
- Fixed timing issue where exportedFilePath was reset too early
This commit is contained in:
saivaraprasadreddy medapati
2026-02-21 01:53:27 +05:30
parent 44cf97c7a1
commit 85f2388041
5 changed files with 93 additions and 7 deletions
+2 -1
View File
@@ -39,7 +39,8 @@ interface Window {
setCurrentVideoPath: (path: string) => Promise<{ success: boolean }>
getCurrentVideoPath: () => Promise<{ success: boolean; path?: string }>
clearCurrentVideoPath: () => Promise<{ success: boolean }>
getPlatform: () => Promise<string>
getPlatform: () => Promise<string>,
revealInFolder: (filePath: string) => Promise<{ success: boolean; error?: string; message?: string }>,
hudOverlayHide: () => void;
hudOverlayClose: () => void;
}
+20
View File
@@ -198,6 +198,26 @@ export function registerIpcHandlers(
}
});
ipcMain.handle('reveal-in-folder', async (_, filePath: string) => {
try {
// shell.showItemInFolder doesn't return a value, it throws on error
shell.showItemInFolder(filePath);
return { success: true };
} catch (error) {
console.error(`Error revealing item in folder: ${filePath}`, error);
// Fallback to open the directory if revealing the item fails
// This might happen if the file was moved or deleted after export,
// or if the path is somehow invalid for showItemInFolder
try {
shell.openPath(path.dirname(filePath));
return { success: true, message: 'Could not reveal item, but opened directory.' };
} catch (openError) {
console.error(`Error opening directory: ${path.dirname(filePath)}`, openError);
return { success: false, error: String(error) };
}
}
});
let currentVideoPath: string | null = null;
ipcMain.handle('set-current-video-path', (_, path: string) => {
+3
View File
@@ -63,4 +63,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
getPlatform: () => {
return ipcRenderer.invoke('get-platform')
},
revealInFolder: (filePath: string) => {
return ipcRenderer.invoke('reveal-in-folder', filePath)
},
})
+34 -1
View File
@@ -2,6 +2,8 @@ import { useEffect, useState } from 'react';
import { X, Download, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import type { ExportProgress } from '@/lib/exporter';
import { toast } from 'sonner'; // Add this import
interface ExportDialogProps {
isOpen: boolean;
@@ -11,6 +13,7 @@ interface ExportDialogProps {
error: string | null;
onCancel?: () => void;
exportFormat?: 'mp4' | 'gif';
exportedFilePath?: string;
}
export function ExportDialog({
@@ -21,6 +24,7 @@ export function ExportDialog({
error,
onCancel,
exportFormat = 'mp4',
exportedFilePath, // Add this line
}: ExportDialogProps) {
const [showSuccess, setShowSuccess] = useState(false);
@@ -77,6 +81,23 @@ export function ExportDialog({
return `Exporting ${formatLabel}`;
};
const handleClickShowInFolder = async () => {
if (exportedFilePath) {
try {
const result = await window.electronAPI.revealInFolder(exportedFilePath);
if (!result.success) {
const errorMessage = result.error || result.message || 'Failed to reveal item in folder.';
console.error('Failed to reveal in folder:', errorMessage);
toast.error(errorMessage);
}
} catch (err) {
const errorMessage = String(err);
console.error('Error calling revealInFolder IPC:', errorMessage);
toast.error(`Error revealing in folder: ${errorMessage}`);
}
}
};
return (
<>
<div
@@ -91,9 +112,21 @@ export function ExportDialog({
<div className="w-12 h-12 rounded-full bg-[#34B27B]/20 flex items-center justify-center ring-1 ring-[#34B27B]/50">
<Download className="w-6 h-6 text-[#34B27B]" />
</div>
<div>
<div className="w-12 h-12 rounded-full bg-[#34B27B]/20 flex items-center justify-center ring-1 ring-[#34B27B]/50">
<Download className="w-6 h-6 text-[#34B27B]" />
</div>
<div className="flex flex-col gap-2"> {/* Added flex container */}
<span className="text-xl font-bold text-slate-200 block">Export Complete</span>
<span className="text-sm text-slate-400">Your {formatLabel.toLowerCase()} is ready</span>
{exportedFilePath && ( // Only show button if path exists
<Button
variant="secondary"
onClick={handleClickShowInFolder}
className="mt-2 w-fit px-3 py-1 text-sm rounded-md bg-white/10 hover:bg-white/20 text-slate-200"
>
Show in Folder
</Button>
)}
</div>
</>
) : (
+34 -5
View File
@@ -65,6 +65,7 @@ export default function VideoEditor() {
const [gifFrameRate, setGifFrameRate] = useState<GifFrameRate>(15);
const [gifLoop, setGifLoop] = useState(true);
const [gifSizePreset, setGifSizePreset] = useState<GifSizePreset>('medium');
const [exportedFilePath, setExportedFilePath] = useState<string | undefined>(undefined);
const videoPlaybackRef = useRef<VideoPlaybackRef>(null);
const nextZoomIdRef = useRef(1);
@@ -510,8 +511,9 @@ export default function VideoEditor() {
if (saveResult.cancelled) {
toast.info('Export cancelled');
} else if (saveResult.success) {
toast.success(`GIF exported successfully to ${saveResult.path}`);
} else if (saveResult.success && saveResult.path) {
showExportSuccessToast(saveResult.path);
setExportedFilePath(saveResult.path);
} else {
setExportError(saveResult.message || 'Failed to save GIF');
toast.error(saveResult.message || 'Failed to save GIF');
@@ -635,8 +637,9 @@ export default function VideoEditor() {
if (saveResult.cancelled) {
toast.info('Export cancelled');
} else if (saveResult.success) {
toast.success(`Video exported successfully to ${saveResult.path}`);
} else if (saveResult.success && saveResult.path) {
showExportSuccessToast(saveResult.path);
setExportedFilePath(saveResult.path);
} else {
setExportError(saveResult.message || 'Failed to save video');
toast.error(saveResult.message || 'Failed to save video');
@@ -709,9 +712,34 @@ export default function VideoEditor() {
setIsExporting(false);
setExportProgress(null);
setExportError(null);
setExportedFilePath(undefined);
}
}, []);
const handleExportDialogClose = useCallback(() => {
setShowExportDialog(false);
setExportedFilePath(undefined);
}, []);
const showExportSuccessToast = useCallback((filePath: string) => {
toast.success(`Video 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)}`);
}
}
}
});
}, []);
if (loading) {
return (
<div className="flex items-center justify-center h-screen bg-background">
@@ -885,12 +913,13 @@ export default function VideoEditor() {
<ExportDialog
isOpen={showExportDialog}
onClose={() => setShowExportDialog(false)}
onClose={handleExportDialogClose}
progress={exportProgress}
isExporting={isExporting}
error={exportError}
onCancel={handleCancelExport}
exportFormat={exportFormat}
exportedFilePath={exportedFilePath}
/>
</div>
);