Breadcrumbs and tooltips

This commit is contained in:
momo5502
2025-04-30 11:50:27 +02:00
parent 62b06a1717
commit 3c380e8420
5 changed files with 195 additions and 125 deletions

View File

@@ -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 (
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<HashRouter>
<Routes>
<Route path="*" element={<Navigate to="/" replace />} />
<Route path="/" element={<LandingPage />} />
<Route path="/playground" element={<Playground />} />
</Routes>
</HashRouter>
<TooltipProvider>
<HashRouter>
<Routes>
<Route path="*" element={<Navigate to="/" replace />} />
<Route path="/" element={<LandingPage />} />
<Route path="/playground" element={<Playground />} />
</Routes>
</HashRouter>
</TooltipProvider>
</ThemeProvider>
);
}

View File

@@ -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<FileWithData[]> {
return Promise.all(promises);
}
function selectFiles(): Promise<FileList> {
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: <HouseFill />,
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<
<Button type="submit" className="fancy rounded-lg">
Create
</Button>
<DialogClose asChild>
<Button variant="secondary" className="fancy rounded-lg">
Cancel
</Button>
</DialogClose>
</DialogFooter>
</form>
</DialogContent>
@@ -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 (
<BreadcrumbItem key={`breadcrumb-page-${index}`}>
<BreadcrumbPage>{e.node}</BreadcrumbPage>
</BreadcrumbItem>
);
}
const navigate = () => this.setState({ path: e.targetPath });
return (
<>
<BreadcrumbItem key={`breadcrumb-${index}`}>
<BreadcrumbLink onClick={navigate}>{e.node}</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator key={`breadcrumb-separator-${index}`} />
</>
);
});
}
_renderBreadCrumb() {
return (
<Breadcrumb>
<BreadcrumbList>{this._renderBreadcrumbElements()}</BreadcrumbList>
</Breadcrumb>
);
}
render() {
const elements = getFolderElements(this.props.filesystem, this.state.path);
@@ -381,7 +482,9 @@ export class FilesystemExplorer extends React.Component<
{this._renderErrorDialog()}
{this._renderRemoveDialog()}
<Dropzone onDrop={this._onFileDrop} noClick={true}>
{this._renderBreadCrumb()}
<Dropzone onDrop={this._uploadFiles} noClick={true}>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
@@ -397,6 +500,7 @@ export class FilesystemExplorer extends React.Component<
renameElementHandler={(e) =>
this.setState({ renameFile: e.name })
}
addFilesHandler={this._onAddFiles}
/>
</div>
)}

View File

@@ -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<UserFile> {
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<FileReader>) {
const arrayBuffer = e.target?.result;
resolve({
name: file.name,
data: arrayBuffer as ArrayBuffer,
});
};
reader.onerror = function (e: ProgressEvent<FileReader>) {
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<Output>(null);
const [settings, setSettings] = useState(createDefaultSettings());
const [emulator, setEmulator] = useState<Emulator | null>(null);
const [drawerOpen, setDrawerOpen] = useState<boolean>(false);
const [filesystem, setFilesystem] = useState<Filesystem | null>(null);
const [filesystemPromise, setFilesystemPromise] =
useState<Promise<Filesystem> | 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<Filesystem>((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 (
<>
<Header
@@ -154,29 +121,9 @@ export function Playground() {
/>
<div className="h-[100dvh] flex flex-col">
<header className="flex shrink-0 items-center gap-2 border-b p-2 overflow-y-auto">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm" className="fancy">
<PlayFill /> Run
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuLabel>Run Application</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
onClick={() => createEmulator("c:/test-sample.exe")}
>
<ImageFill className="mr-2" />
<span>Select Sample</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => loadAndRunUserFile()}>
<FileEarmarkCheckFill className="mr-2" />
<span>Select your .exe</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<Button size="sm" className="fancy" onClick={start}>
<PlayFill /> Start
</Button>
<Button
disabled={!emulator}
@@ -219,12 +166,7 @@ export function Playground() {
{!filesystem ? (
<></>
) : (
<Drawer>
<DrawerTrigger asChild>
<Button size="sm" variant="secondary" className="fancy">
<GearFill /> Filesystem
</Button>
</DrawerTrigger>
<Drawer open={drawerOpen} onOpenChange={setDrawerOpen}>
<DrawerContent>
<DrawerHeader>
<DrawerTitle className="hidden">
@@ -238,6 +180,8 @@ export function Playground() {
<FilesystemExplorer
filesystem={filesystem}
runFile={createEmulator}
resetFilesys={resetFilesys}
path={["c"]}
/>
</DrawerFooter>
</DrawerContent>

View File

@@ -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 (
<ContextMenu>
<ContextMenuTrigger>
<Tooltip>
<Tooltip delayDuration={700}>
<TooltipTrigger asChild>
{renderElement(element, props.clickHandler)}
</TooltipTrigger>
@@ -131,31 +133,26 @@ function renderElementWrapper(element: FolderElement, props: FolderProps) {
);
}
function renderFolderCreator(createFolderHandler: CreateFolderHandler) {
return (
<ContextMenuItem onClick={createFolderHandler}>
Create new Folder
</ContextMenuItem>
);
}
export function Folder(props: FolderProps) {
return (
<ScrollArea className="h-[50dvh]">
<TooltipProvider delayDuration={700}>
<ContextMenu>
<ContextMenuTrigger>
<div className="folder flex flex-wrap">
{props.elements
.sort(elementComparator)
.map((e) => renderElementWrapper(e, props))}
</div>
</ContextMenuTrigger>
<ContextMenuContent>
{renderFolderCreator(props.createFolderHandler)}
</ContextMenuContent>
</ContextMenu>
</TooltipProvider>
</ScrollArea>
<ContextMenu>
<ContextMenuTrigger>
<ScrollArea className="h-[50dvh]">
<div className="folder flex flex-wrap h-full">
{props.elements
.sort(elementComparator)
.map((e) => renderElementWrapper(e, props))}
</div>
</ScrollArea>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onClick={props.createFolderHandler}>
Create new Folder
</ContextMenuItem>
<ContextMenuItem onClick={props.addFilesHandler}>
Add Files
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
}

View File

@@ -43,6 +43,24 @@ export interface FileWithData {
data: ArrayBuffer;
}
function deleteDatabase(dbName: string) {
return new Promise<void>((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) {