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>
<head>
<meta charset="UTF-8" />
<title>Miller code editor</title>
</head>
<body>
<header>Miller code editor - welcome!
<header>
Miller code editor - welcome!
<button id="addEditor">Add Editor</button>
<button id="openFolder">Open Folder</button>
<strong id="currentFolder">N/A</strong>
</header>
<aside>
<nav>
<p>Files go here</p>
<p>foo</p>
<p>bar</p>
</nav>
<nav></nav>
</aside>
<main id="editorGrid">
</main>
<main id="editorGrid"></main>
<script type="module" src="/src/app/renderer.ts"></script>
</body>
</html>

View File

@ -1,7 +1,94 @@
import van from "vanjs-core";
const v = van.tags;
import type { FolderTree } from "../types/global";
document.getElementById("openFolder").addEventListener("click", async () => {
const folderPath = await window.electronAPI.openFolder();
document.getElementById("currentFolder").innerText = folderPath;
});
const folderTreeState = van.state<FolderTree | null>(null);
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 */
#editorGrid {
display: grid;
height: minmax(0, 100%);
@ -52,3 +51,16 @@ aside {
}
/* 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 fs from "fs";
const fsp = fs.promises;
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, {
properties: ["openDirectory"],
});
if (!result.canceled && result.filePaths.length > 0) {
const folderPath = result.filePaths[0];
return folderPath;
return {
name: path.basename(folderPath),
path: folderPath,
type: "directory",
children: await readTree(folderPath),
};
}
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 { handleOpenFolder, readFolderContents } from "./fileOperations";
import { handleOpenFolder } from "./fileOperations";
import path from "node:path";
import started from "electron-squirrel-startup";
@ -45,13 +45,13 @@ app.whenReady().then(() => {
return await handleOpenFolder(senderWindow);
});
createWindow();
if (process.platform === "darwin") {
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
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.

View File

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

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

@ -1,9 +1,21 @@
// 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
interface Window {
declare global {
interface Window {
electronAPI: {
openFolder: () => Promise<string>;
openFolder: () => Promise<FolderTree | null>;
// Add other methods as needed
};
}
}
export { FolderTree };

View File

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