mirror of
https://github.com/momo5502/emulator.git
synced 2026-01-20 20:23:57 +00:00
Breadcrumbs and tooltips
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user