Merge pull request #163 from varaprasadreddy9676/feature/reveal-export-folder

feat: add reveal in folder option after export
This commit is contained in:
Sid
2026-03-04 22:48:17 -08:00
committed by GitHub
5 changed files with 113 additions and 11 deletions
+10 -9
View File
@@ -35,18 +35,19 @@ interface Window {
getCursorTelemetry: (videoPath?: string) => Promise<{ success: boolean; samples: CursorTelemetryPoint[]; message?: string; error?: string }>
onStopRecordingFromTray: (callback: () => void) => () => void
openExternalUrl: (url: string) => Promise<{ success: boolean; error?: string }>
saveExportedVideo: (videoData: ArrayBuffer, fileName: string) => Promise<{ success: boolean; path?: string; message?: string; canceled?: boolean }>
openVideoFilePicker: () => Promise<{ success: boolean; path?: string; canceled?: boolean }>
setCurrentVideoPath: (path: string) => Promise<{ success: boolean }>
getCurrentVideoPath: () => Promise<{ success: boolean; path?: string }>
clearCurrentVideoPath: () => Promise<{ success: boolean }>
saveProjectFile: (projectData: unknown, suggestedName?: string, existingProjectPath?: string) => Promise<{ success: boolean; path?: string; message?: string; canceled?: boolean; error?: string }>
loadProjectFile: () => Promise<{ success: boolean; path?: string; project?: unknown; message?: string; canceled?: boolean; error?: string }>
loadCurrentProjectFile: () => Promise<{ success: boolean; path?: string; project?: unknown; message?: string; canceled?: boolean; error?: string }>
onMenuLoadProject: (callback: () => void) => () => void
saveExportedVideo: (videoData: ArrayBuffer, fileName: string) => Promise<{ success: boolean; path?: string; message?: string; canceled?: boolean }>
openVideoFilePicker: () => Promise<{ success: boolean; path?: string; canceled?: boolean }>
setCurrentVideoPath: (path: string) => Promise<{ success: boolean }>
getCurrentVideoPath: () => Promise<{ success: boolean; path?: string }>
clearCurrentVideoPath: () => Promise<{ success: boolean }>
saveProjectFile: (projectData: unknown, suggestedName?: string, existingProjectPath?: string) => Promise<{ success: boolean; path?: string; message?: string; canceled?: boolean; error?: string }>
loadProjectFile: () => Promise<{ success: boolean; path?: string; project?: unknown; message?: string; canceled?: boolean; error?: string }>
loadCurrentProjectFile: () => Promise<{ success: boolean; path?: string; project?: unknown; message?: string; canceled?: boolean; error?: string }>
onMenuLoadProject: (callback: () => void) => () => void
onMenuSaveProject: (callback: () => void) => () => void
onMenuSaveProjectAs: (callback: () => void) => () => void
getPlatform: () => Promise<string>
revealInFolder: (filePath: string) => Promise<{ success: boolean; error?: string; message?: string }>,
getShortcuts: () => Promise<Record<string, unknown> | null>
saveShortcuts: (shortcuts: unknown) => Promise<{ success: boolean; error?: string }>
hudOverlayHide: () => void;
+25
View File
@@ -333,6 +333,31 @@ 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 {
const openPathResult = await shell.openPath(path.dirname(filePath));
if (openPathResult) {
// openPath returned an error message
return { success: false, error: openPathResult };
}
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('save-project-file', async (_, projectData: unknown, suggestedName?: string, existingProjectPath?: string) => {
try {
const trustedExistingProjectPath = isTrustedProjectPath(existingProjectPath)
+4
View File
@@ -90,6 +90,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
getPlatform: () => {
return ipcRenderer.invoke('get-platform')
},
revealInFolder: (filePath: string) => {
return ipcRenderer.invoke('reveal-in-folder', filePath)
},
})
getShortcuts: () => {
return ipcRenderer.invoke('get-shortcuts')
},
+36 -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,23 @@ 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="flex flex-col gap-2">
<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 && (
<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>
)}
{exportedFilePath && (
<span className="text-xs text-slate-500 break-all max-w-xs mt-1">
{exportedFilePath.split('/').pop()}
</span>
)}
</div>
</>
) : (
+38 -1
View File
@@ -82,6 +82,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 [lastSavedSnapshot, setLastSavedSnapshot] = useState<string | null>(null);
const videoPlaybackRef = useRef<VideoPlaybackRef>(null);
@@ -882,6 +883,11 @@ export default function VideoEditor() {
const saveResult = await window.electronAPI.saveExportedVideo(arrayBuffer, fileName);
if (saveResult.cancelled) {
toast.info('Export cancelled');
} else if (saveResult.success && saveResult.path) {
showExportSuccessToast(saveResult.path);
setExportedFilePath(saveResult.path);
if (saveResult.canceled) {
toast.info('Export canceled');
} else if (saveResult.success) {
@@ -1008,6 +1014,11 @@ export default function VideoEditor() {
const saveResult = await window.electronAPI.saveExportedVideo(arrayBuffer, fileName);
if (saveResult.cancelled) {
toast.info('Export cancelled');
} else if (saveResult.success && saveResult.path) {
showExportSuccessToast(saveResult.path);
setExportedFilePath(saveResult.path);
if (saveResult.canceled) {
toast.info('Export canceled');
} else if (saveResult.success) {
@@ -1084,9 +1095,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(`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">
@@ -1285,12 +1321,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>
);