feat(preload): add file.list, file.pickFolder, file.pickFile convenience methods

This commit is contained in:
2026-06-05 15:13:16 +05:30
parent 650c266945
commit 3598d06240
7 changed files with 241 additions and 8 deletions
+49
View File
@@ -3039,6 +3039,55 @@ ipcMain.on('select-folder', (event, type) => {
}
});
// List directory contents as FileEntry[]
ipcMain.handle('list-directory', async (event, dirPath) => {
try {
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
return entries.map((dirent) => {
const fullPath = path.join(dirPath, dirent.name);
let size;
let modifiedAt;
if (!dirent.isDirectory()) {
try {
const stat = fs.statSync(fullPath);
size = stat.size;
modifiedAt = stat.mtime.toISOString();
} catch {
// ignore stat errors
}
}
return {
name: dirent.name,
path: fullPath,
isDirectory: dirent.isDirectory(),
size,
modifiedAt,
};
});
} catch (err) {
return { error: err.message };
}
});
// Show folder picker dialog and return selected path or null
ipcMain.handle('pick-folder', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory'],
});
if (result.canceled || result.filePaths.length === 0) return null;
return result.filePaths[0];
});
// Show file picker dialog for markdown files and return selected path or null
ipcMain.handle('pick-file', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
filters: [{ name: 'Markdown', extensions: ['md', 'markdown', 'mdown', 'mkd'] }],
});
if (result.canceled || result.filePaths.length === 0) return null;
return result.filePaths[0];
});
ipcMain.on('export-spreadsheet', (event, { content, format }) => {
const outputFile = dialog.showSaveDialogSync(mainWindow, {
defaultPath: currentFile.replace(/\.[^/.]+$/, `.${format}`),
+10 -2
View File
@@ -237,7 +237,12 @@ const ALLOWED_RECEIVE_CHANNELS = [
'load-template-menu',
'toggle-command-palette',
'toggle-sidebar-panel',
'toggle-bottom-panel'
'toggle-bottom-panel',
// File dialog / directory listing
'list-directory',
'pick-folder',
'pick-file'
];
/**
@@ -344,7 +349,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
exists: (filePath) => ipcRenderer.invoke('path-exists', filePath),
isDirectory: (filePath) => ipcRenderer.invoke('is-directory', filePath),
copy: (source, destination) => ipcRenderer.invoke('copy-path', { source, destination }),
move: (source, destination) => ipcRenderer.invoke('move-path', { source, destination })
move: (source, destination) => ipcRenderer.invoke('move-path', { source, destination }),
list: (dirPath) => ipcRenderer.invoke('list-directory', dirPath),
pickFolder: () => ipcRenderer.invoke('pick-folder'),
pickFile: () => ipcRenderer.invoke('pick-file')
},
// Theme Operations
+8 -4
View File
@@ -1,4 +1,5 @@
import { ChevronRight } from 'lucide-react';
import { ChevronRight, FolderOpen } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui/collapsible';
import { ScrollArea } from '@/components/ui/scroll-area';
import { useFileStore } from '@/stores/file-store';
@@ -6,7 +7,7 @@ import { FileTree } from './FileTree';
import { Outline } from './Outline';
export function Sidebar() {
const rootPath = useFileStore((s) => s.rootPath);
const tree = useFileStore((s) => s.tree);
return (
<div className="flex h-full flex-col gap-3">
@@ -20,9 +21,12 @@ export function Sidebar() {
</CollapsibleTrigger>
<CollapsibleContent>
<ScrollArea className="h-[calc(100vh-240px)]">
{!rootPath ? (
<div className="flex flex-col items-center justify-center gap-2 py-8 text-xs text-muted-foreground">
{!tree ? (
<div className="flex flex-col items-center gap-2 p-4 text-xs text-muted-foreground">
<span>No folder opened</span>
<Button size="sm" variant="outline" onClick={() => useFileStore.getState().openFolderDialog()}>
<FolderOpen className="mr-1" /> Open Folder
</Button>
</div>
) : (
<FileTree />
+4
View File
@@ -66,6 +66,10 @@ export const ipc = {
safeCall('file', 'write', path, content),
list: (dir: string): Promise<IpcResult<FileEntry[] | ChannelMissing>> =>
safeCall('file', 'list', dir),
pickFolder: (): Promise<IpcResult<string | null | ChannelMissing>> =>
safeCall('file', 'pickFolder'),
pickFile: (): Promise<IpcResult<string | null | ChannelMissing>> =>
safeCall('file', 'pickFile'),
onChange: (cb: (path: string) => void): (() => void) => {
if (typeof window === 'undefined' || !window.electronAPI?.file?.onChange) {
return () => {};
+30
View File
@@ -39,6 +39,9 @@ interface FileState {
markTabClean: (id: string) => void;
markTabDirty: (id: string) => void;
reorderTabs: (fromIndex: number, toIndex: number) => void;
openFolderDialog: () => Promise<void>;
openFileDialog: () => Promise<void>;
saveActiveBuffer: () => Promise<boolean>;
}
function entryToNode(entry: FileEntry): FileNode {
@@ -191,5 +194,32 @@ export const useFileStore = create<FileState>()(
tabs.splice(toIndex, 0, moved);
});
},
openFolderDialog: async () => {
const result = await ipc.file.pickFolder();
if (!result.ok || result.data === null) return;
await useFileStore.getState().openFolder(result.data);
},
openFileDialog: async () => {
const result = await ipc.file.pickFile();
if (!result.ok || result.data === null) return;
await useFileStore.getState().openFile(result.data);
},
saveActiveBuffer: async () => {
const { activeTabId } = useFileStore.getState();
if (!activeTabId) return false;
const buffer = useEditorStore.getState().buffers.get(activeTabId);
if (!buffer) return false;
const writeResult = await ipc.file.write(activeTabId, buffer.content);
if (!writeResult.ok) return false;
useEditorStore.getState().markSaved(activeTabId);
useFileStore.getState().markTabClean(activeTabId);
return true;
},
}))
);