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:
Vendored
+2
-1
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -63,4 +63,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
getPlatform: () => {
|
||||
return ipcRenderer.invoke('get-platform')
|
||||
},
|
||||
revealInFolder: (filePath: string) => {
|
||||
return ipcRenderer.invoke('reveal-in-folder', filePath)
|
||||
},
|
||||
})
|
||||
@@ -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>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user