diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index dba3f16..5868160 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -39,7 +39,8 @@ interface Window { setCurrentVideoPath: (path: string) => Promise<{ success: boolean }> getCurrentVideoPath: () => Promise<{ success: boolean; path?: string }> clearCurrentVideoPath: () => Promise<{ success: boolean }> - getPlatform: () => Promise + getPlatform: () => Promise, + revealInFolder: (filePath: string) => Promise<{ success: boolean; error?: string; message?: string }>, hudOverlayHide: () => void; hudOverlayClose: () => void; } diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 867b72b..5c9827c 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -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) => { diff --git a/electron/preload.ts b/electron/preload.ts index 02fcc97..667965c 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -63,4 +63,7 @@ contextBridge.exposeInMainWorld('electronAPI', { getPlatform: () => { return ipcRenderer.invoke('get-platform') }, + revealInFolder: (filePath: string) => { + return ipcRenderer.invoke('reveal-in-folder', filePath) + }, }) \ No newline at end of file diff --git a/src/components/video-editor/ExportDialog.tsx b/src/components/video-editor/ExportDialog.tsx index 417b4d2..af9dde8 100644 --- a/src/components/video-editor/ExportDialog.tsx +++ b/src/components/video-editor/ExportDialog.tsx @@ -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 ( <>
-
+
+ +
+
{/* Added flex container */} Export Complete Your {formatLabel.toLowerCase()} is ready + {exportedFilePath && ( // Only show button if path exists + + )}
) : ( diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index c0a038e..354cda9 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -65,6 +65,7 @@ export default function VideoEditor() { const [gifFrameRate, setGifFrameRate] = useState(15); const [gifLoop, setGifLoop] = useState(true); const [gifSizePreset, setGifSizePreset] = useState('medium'); + const [exportedFilePath, setExportedFilePath] = useState(undefined); const videoPlaybackRef = useRef(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 (
@@ -885,12 +913,13 @@ export default function VideoEditor() { setShowExportDialog(false)} + onClose={handleExportDialogClose} progress={exportProgress} isExporting={isExporting} error={exportError} onCancel={handleCancelExport} exportFormat={exportFormat} + exportedFilePath={exportedFilePath} />
);