add partially working file browser

This commit is contained in:
Quinten Kock 2025-08-13 05:08:44 +02:00
parent c765b34019
commit 0d874a7cf1
8 changed files with 195 additions and 53 deletions

View File

@ -1,24 +1,18 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Miller code editor</title> <title>Miller code editor</title>
</head> </head>
<body> <body>
<header>Miller code editor - welcome! <header>
<button id="addEditor">Add Editor</button> Miller code editor - welcome!
<button id="openFolder">Open Folder</button> <button id="addEditor">Add Editor</button>
<strong id="currentFolder">N/A</strong> </header>
</header> <aside>
<aside> <nav></nav>
<nav> </aside>
<p>Files go here</p> <main id="editorGrid"></main>
<p>foo</p> <script type="module" src="/src/app/renderer.ts"></script>
<p>bar</p> </body>
</nav>
</aside>
<main id="editorGrid">
</main>
<script type="module" src="/src/app/renderer.ts"></script>
</body>
</html> </html>

View File

@ -1,7 +1,94 @@
import van from "vanjs-core"; import van from "vanjs-core";
const v = van.tags; const v = van.tags;
import type { FolderTree } from "../types/global";
document.getElementById("openFolder").addEventListener("click", async () => { const folderTreeState = van.state<FolderTree | null>(null);
const folderPath = await window.electronAPI.openFolder();
document.getElementById("currentFolder").innerText = folderPath; async function openFolder() {
}); const folderTree = await window.electronAPI.openFolder();
if (!folderTree) return;
folderTreeState.val = folderTree;
}
const FolderTreeView = () => {
if (!folderTreeState.val) {
return v.div(
{ style: "text-align: center; margin-top: 25px;" },
v.p("No folder selected!"),
v.p(
v.button(
{
onclick: async () => {
const folderTree =
await window.electronAPI.openFolder();
if (!folderTree) return;
folderTreeState.val = folderTree;
},
},
"Open Folder",
),
),
);
}
return v.div(
v.span(
{ style: "font-weight: bold; margin-right: 1em;" },
folderTreeState.val?.name ?? "No folder",
),
v.button(
{
onclick: openFolder,
title: "Refresh current folder",
style: "margin-right: 0.5em;",
},
"⟳",
),
v.button(
{
onclick: openFolder,
title: "Open another folder",
style: "margin-right: 0.5em;",
},
"📁",
),
folderTreeState.val.children?.map(FsItemView) || [],
);
};
const FsItemView = (tree: FolderTree): HTMLElement | string => {
if (tree.type === "file") return v.p(tree.name);
return v.details(v.summary(tree.name), tree.children?.map(FsItemView));
// if (tree.type === "file") return v.div(" ", tree.name);
// // Use a state to track open/close instead of <details>
// const isOpen = van.state(0);
// return v.div(
// {
// style: "cursor: pointer; user-select: none; font-weight: bold;",
// onclick: () => isOpen.val++,
// },
// () => (isOpen.val ? "▼ " : "▶ "),
// tree.name,
// );
// // return v.div(
// // v.span(
// // {
// // style: "cursor: pointer; user-select: none; font-weight: bold;",
// // onclick: () => {
// // console.log("opening");
// // isOpen.val++;
// // },
// // },
// // () => (isOpen.val ? "▼ " : "▶ "),
// // tree.name,
// // ),
// // " ",
// // isOpen.val,
// // );
};
// Mount the folder tree view reactively to the nav
const nav = document.querySelector("aside nav");
if (nav) {
van.add(nav, () => FolderTreeView());
}

View File

@ -33,7 +33,6 @@ aside {
} }
/* Editor grid stuff */ /* Editor grid stuff */
#editorGrid { #editorGrid {
display: grid; display: grid;
height: minmax(0, 100%); height: minmax(0, 100%);
@ -52,3 +51,16 @@ aside {
} }
/* Navbar styling */ /* Navbar styling */
nav details {
margin-bottom: 0px;
}
nav details[open] summary,
nav details summary,
nav div p {
margin-bottom: 5px;
line-height: 1.2;
}
nav details[open] :not(summary) {
margin-left: 16px;
}
/* closed detail summary */

View File

@ -3,26 +3,57 @@
import { dialog, BrowserWindow } from "electron"; import { dialog, BrowserWindow } from "electron";
import fs from "fs"; import fs from "fs";
const fsp = fs.promises;
import path from "path"; import path from "path";
export async function handleOpenFolder(mainWindow: BrowserWindow) { type FolderTree = {
name: string;
path: string;
type: "directory" | "file";
children?: FolderTree[];
};
async function readTree(dirPath: string): Promise<FolderTree[]> {
const stats = await fsp.stat(dirPath);
if (!stats.isDirectory()) return [];
const names = await fsp.readdir(dirPath);
const children = await Promise.all(
names.map(async (name) => {
const fullPath = path.join(dirPath, name);
const stat = await fsp.stat(fullPath);
if (stat.isDirectory()) {
return {
name,
path: fullPath,
type: "directory" as const,
children: await readTree(fullPath),
};
} else {
return {
name,
path: fullPath,
type: "file" as const,
};
}
})
);
return children;
}
export async function handleOpenFolder(
mainWindow: BrowserWindow,
): Promise<FolderTree | null> {
const result = await dialog.showOpenDialog(mainWindow, { const result = await dialog.showOpenDialog(mainWindow, {
properties: ["openDirectory"], properties: ["openDirectory"],
}); });
if (!result.canceled && result.filePaths.length > 0) { if (!result.canceled && result.filePaths.length > 0) {
const folderPath = result.filePaths[0]; const folderPath = result.filePaths[0];
return folderPath; return {
name: path.basename(folderPath),
path: folderPath,
type: "directory",
children: await readTree(folderPath),
};
} }
return null; return null;
} }
export function readFolderContents(folderPath: string): string[] {
try {
return fs
.readdirSync(folderPath)
.map((file) => path.join(folderPath, file));
} catch (err) {
console.error("Error reading folder:", err);
return [];
}
}

View File

@ -1,5 +1,5 @@
import { app, BrowserWindow, ipcMain } from "electron"; import { app, BrowserWindow, ipcMain } from "electron";
import { handleOpenFolder, readFolderContents } from "./fileOperations"; import { handleOpenFolder } from "./fileOperations";
import path from "node:path"; import path from "node:path";
import started from "electron-squirrel-startup"; import started from "electron-squirrel-startup";
@ -45,13 +45,13 @@ app.whenReady().then(() => {
return await handleOpenFolder(senderWindow); return await handleOpenFolder(senderWindow);
}); });
createWindow(); createWindow();
app.on("activate", function () { if (process.platform === "darwin") {
if (BrowserWindow.getAllWindows().length === 0) createWindow(); app.on("activate", function () {
}); if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
}
}); });
app.on("ready", createWindow);
// Quit when all windows are closed, except on macOS. There, it's common // Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits // for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q. // explicitly with Cmd + Q.

View File

@ -2,7 +2,9 @@
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts // https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
import { contextBridge, ipcRenderer } from "electron"; import { contextBridge, ipcRenderer } from "electron";
import type { FolderTree } from "./types/global";
contextBridge.exposeInMainWorld("electronAPI", { contextBridge.exposeInMainWorld("electronAPI", {
openFolder: () => ipcRenderer.invoke("dialog:openFolder"), openFolder: () =>
ipcRenderer.invoke("dialog:openFolder") as Promise<FolderTree | null>,
}); });

22
src/types/global.d.ts vendored
View File

@ -1,9 +1,21 @@
// src/types/global.d.ts // src/types/global.d.ts
// FolderTree type for folder structure
type FolderTree = {
name: string;
path: string;
type: "directory" | "file";
children?: FolderTree[];
};
// Extend the Window interface to include electronAPI // Extend the Window interface to include electronAPI
interface Window { declare global {
electronAPI: { interface Window {
openFolder: () => Promise<string>; electronAPI: {
// Add other methods as needed openFolder: () => Promise<FolderTree | null>;
}; // Add other methods as needed
};
}
} }
export { FolderTree };

View File

@ -11,5 +11,9 @@
"outDir": "dist", "outDir": "dist",
"moduleResolution": "node", "moduleResolution": "node",
"resolveJsonModule": true "resolveJsonModule": true
} },
"include": [
"src/types/global.d.ts",
"src/**/*.ts"
]
} }