From 3c380e84208445fba198a086725f92f0b7ab06cb Mon Sep 17 00:00:00 2001 From: momo5502 Date: Wed, 30 Apr 2025 11:50:27 +0200 Subject: [PATCH] Breadcrumbs and tooltips --- page/src/App.tsx | 17 +++-- page/src/FilesystemExplorer.tsx | 112 +++++++++++++++++++++++++++-- page/src/Playground.tsx | 120 +++++++++----------------------- page/src/components/folder.tsx | 49 ++++++------- page/src/filesystem.ts | 22 ++++++ 5 files changed, 195 insertions(+), 125 deletions(-) diff --git a/page/src/App.tsx b/page/src/App.tsx index 17a58b11..e71e2107 100644 --- a/page/src/App.tsx +++ b/page/src/App.tsx @@ -1,4 +1,5 @@ import { ThemeProvider } from "@/components/theme-provider"; +import { TooltipProvider } from "@/components/ui/tooltip"; import { HashRouter, Route, Routes, Navigate } from "react-router-dom"; import { Playground } from "./Playground"; import { LandingPage } from "./LandingPage"; @@ -18,13 +19,15 @@ import "./App.css"; function App() { return ( - - - } /> - } /> - } /> - - + + + + } /> + } /> + } /> + + + ); } diff --git a/page/src/FilesystemExplorer.tsx b/page/src/FilesystemExplorer.tsx index a747d0ce..9255fbc9 100644 --- a/page/src/FilesystemExplorer.tsx +++ b/page/src/FilesystemExplorer.tsx @@ -15,10 +15,22 @@ import { Input } from "./components/ui/input"; import { DialogDescription } from "@radix-ui/react-dialog"; import Dropzone from "react-dropzone"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; + +import { HouseFill } from "react-bootstrap-icons"; export interface FilesystemExplorerProps { filesystem: Filesystem; runFile: (file: string) => void; + resetFilesys: () => void; + path: string[]; } export interface FilesystemExplorerState { path: string[]; @@ -128,6 +140,43 @@ async function readFiles(files: FileList | File[]): Promise { return Promise.all(promises); } +function selectFiles(): Promise { + return new Promise((resolve) => { + const fileInput = document.createElement("input"); + fileInput.type = "file"; + fileInput.accept = ".exe"; + + fileInput.addEventListener("change", function (event) { + const files = (event as any).target.files as FileList; + resolve(files); + }); + + fileInput.click(); + }); +} + +interface BreadcrumbElement { + node: React.ReactNode; + targetPath: string[]; +} + +function generateBreadcrumbElements(path: string[]): BreadcrumbElement[] { + const elements = path.map((p, index) => { + const e: BreadcrumbElement = { + node: p, + targetPath: path.slice(0, index + 1), + }; + + return e; + }); + elements.unshift({ + node: , + targetPath: [], + }); + + return elements; +} + export class FilesystemExplorer extends React.Component< FilesystemExplorerProps, FilesystemExplorerState @@ -135,11 +184,12 @@ export class FilesystemExplorer extends React.Component< constructor(props: FilesystemExplorerProps) { super(props); - this._onFileDrop = this._onFileDrop.bind(this); + this._onAddFiles = this._onAddFiles.bind(this); + this._uploadFiles = this._uploadFiles.bind(this); this._onElementSelect = this._onElementSelect.bind(this); this.state = { - path: [], + path: this.props.path, createFolder: false, errorText: "", removeFile: "", @@ -186,6 +236,11 @@ export class FilesystemExplorer extends React.Component< this.forceUpdate(); } + async _onAddFiles() { + const files = await selectFiles(); + await this._uploadFiles(files); + } + async _onFolderCreate(name: string) { this.setState({ createFolder: false }); @@ -210,7 +265,16 @@ export class FilesystemExplorer extends React.Component< this.forceUpdate(); } - async _onFileDrop(files: FileList | File[]) { + async _uploadFiles(files: FileList | File[]) { + if (files.length == 0) { + return; + } + + if (this.state.path.length == 0) { + this._showError("Files must be within a drive"); + return; + } + const fileData = (await readFiles(files)).map((f) => { return { name: makeFullPathWithState(this.state, f.file.name.toLowerCase()), @@ -249,6 +313,11 @@ export class FilesystemExplorer extends React.Component< + + + @@ -371,6 +440,38 @@ export class FilesystemExplorer extends React.Component< ); } + _renderBreadcrumbElements() { + const elements = generateBreadcrumbElements(this.state.path); + + return elements.map((e, index) => { + if (index == this.state.path.length) { + return ( + + {e.node} + + ); + } + + const navigate = () => this.setState({ path: e.targetPath }); + return ( + <> + + {e.node} + + + + ); + }); + } + + _renderBreadCrumb() { + return ( + + {this._renderBreadcrumbElements()} + + ); + } + render() { const elements = getFolderElements(this.props.filesystem, this.state.path); @@ -381,7 +482,9 @@ export class FilesystemExplorer extends React.Component< {this._renderErrorDialog()} {this._renderRemoveDialog()} - + {this._renderBreadCrumb()} + + {({ getRootProps, getInputProps }) => (
@@ -397,6 +500,7 @@ export class FilesystemExplorer extends React.Component< renameElementHandler={(e) => this.setState({ renameFile: e.name }) } + addFilesHandler={this._onAddFiles} />
)} diff --git a/page/src/Playground.tsx b/page/src/Playground.tsx index 04748301..ab15d6ba 100644 --- a/page/src/Playground.tsx +++ b/page/src/Playground.tsx @@ -16,92 +16,62 @@ import { import { createDefaultSettings } from "./settings"; import { SettingsMenu } from "@/components/settings-menu"; -import { - PlayFill, - StopFill, - GearFill, - PauseFill, - FileEarmarkCheckFill, - ImageFill, -} from "react-bootstrap-icons"; +import { PlayFill, StopFill, GearFill, PauseFill } from "react-bootstrap-icons"; import { StatusIndicator } from "@/components/status-indicator"; import { Header } from "./Header"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, - DropdownMenuGroup, -} from "@/components/ui/dropdown-menu"; import { Button } from "@/components/ui/button"; import { Drawer, - DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, - DrawerTrigger, } from "@/components/ui/drawer"; import { FilesystemExplorer } from "./FilesystemExplorer"; -/*function selectAndReadFile(): Promise { - return new Promise((resolve, reject) => { - const fileInput = document.createElement("input"); - fileInput.type = "file"; - fileInput.accept = ".exe"; - - fileInput.addEventListener("change", function (event) { - const file = (event as any).target.files[0]; - if (file) { - const reader = new FileReader(); - - reader.onload = function (e: ProgressEvent) { - const arrayBuffer = e.target?.result; - resolve({ - name: file.name, - data: arrayBuffer as ArrayBuffer, - }); - }; - - reader.onerror = function (e: ProgressEvent) { - reject(new Error("Error reading file: " + e.target?.error)); - }; - - reader.readAsArrayBuffer(file); - } else { - reject(new Error("No file selected")); - } - }); - - fileInput.click(); - }); -}*/ - export function Playground() { const output = useRef(null); const [settings, setSettings] = useState(createDefaultSettings()); const [emulator, setEmulator] = useState(null); + const [drawerOpen, setDrawerOpen] = useState(false); const [filesystem, setFilesystem] = useState(null); const [filesystemPromise, setFilesystemPromise] = useState | null>(null); const [, forceUpdate] = useReducer((x) => x + 1, 0); - if (!filesystemPromise) { + async function resetFilesys() { + const fs = await initFilesys(); + await fs.delete(); + + setFilesystem(null); + setFilesystemPromise(null); + setDrawerOpen(false); + } + + function initFilesys() { + if (filesystemPromise) { + return filesystemPromise; + } + const promise = new Promise((resolve) => { + logLine("Loading filesystem..."); setupFilesystem((current, total, file) => { logLine(`Processing filesystem (${current}/${total}): ${file}`); }).then(resolve); }); promise.then(setFilesystem); - setFilesystemPromise(promise); + + return promise; + } + + async function start() { + await initFilesys(); + setDrawerOpen(true); } function logLine(line: string) { @@ -128,6 +98,8 @@ export function Playground() { emulator?.stop(); output.current?.clear(); + setDrawerOpen(false); + logLine("Starting emulation..."); if (filesystemPromise) { @@ -141,11 +113,6 @@ export function Playground() { new_emulator.start(settings, userFile); } - async function loadAndRunUserFile() { - //const fileBuffer = await selectAndReadFile(); - //await createEmulator(fileBuffer); - } - return ( <>
- - - - - - Run Application - - - createEmulator("c:/test-sample.exe")} - > - - Select Sample - - loadAndRunUserFile()}> - - Select your .exe - - - - + - + @@ -238,6 +180,8 @@ export function Playground() { diff --git a/page/src/components/folder.tsx b/page/src/components/folder.tsx index a32ea608..3c92d8ed 100644 --- a/page/src/components/folder.tsx +++ b/page/src/components/folder.tsx @@ -14,10 +14,10 @@ import { ContextMenuSeparator, ContextMenuLabel, } from "@/components/ui/context-menu"; + import { Tooltip, TooltipContent, - TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; @@ -35,6 +35,7 @@ type ClickHandler = (element: FolderElement) => void; type CreateFolderHandler = () => void; type RemoveElementHandler = (element: FolderElement) => void; type RenameElementHandler = (element: FolderElement) => void; +type AddFilesHandler = () => void; export interface FolderProps { elements: FolderElement[]; @@ -42,6 +43,7 @@ export interface FolderProps { createFolderHandler: CreateFolderHandler; removeElementHandler: RemoveElementHandler; renameElementHandler: RenameElementHandler; + addFilesHandler: AddFilesHandler; } function elementComparator(e1: FolderElement, e2: FolderElement) { @@ -100,7 +102,7 @@ function renderElementWithContext(element: FolderElement, props: FolderProps) { return ( - + {renderElement(element, props.clickHandler)} @@ -131,31 +133,26 @@ function renderElementWrapper(element: FolderElement, props: FolderProps) { ); } -function renderFolderCreator(createFolderHandler: CreateFolderHandler) { - return ( - - Create new Folder - - ); -} - export function Folder(props: FolderProps) { return ( - - - - -
- {props.elements - .sort(elementComparator) - .map((e) => renderElementWrapper(e, props))} -
-
- - {renderFolderCreator(props.createFolderHandler)} - -
-
-
+ + + +
+ {props.elements + .sort(elementComparator) + .map((e) => renderElementWrapper(e, props))} +
+
+
+ + + Create new Folder + + + Add Files + + +
); } diff --git a/page/src/filesystem.ts b/page/src/filesystem.ts index 45d2c3c8..34302912 100644 --- a/page/src/filesystem.ts +++ b/page/src/filesystem.ts @@ -43,6 +43,24 @@ export interface FileWithData { data: ArrayBuffer; } +function deleteDatabase(dbName: string) { + return new Promise((resolve, reject) => { + const request = indexedDB.deleteDatabase(dbName); + + request.onsuccess = () => { + resolve(); + }; + + request.onerror = () => { + reject(new Error(`Error deleting database ${dbName}.`)); + }; + + request.onblocked = () => { + reject(new Error(`Deletion of database ${dbName} blocked.`)); + }; + }); +} + export class Filesystem { private idbfs: MainModule; @@ -117,6 +135,10 @@ export class Filesystem { isFolder(file: string) { return (this.stat(file).mode & 0x4000) != 0; } + + delete() { + return deleteDatabase("/root"); + } } export async function setupFilesystem(progressHandler: ProgressHandler) {