Prepare filesystem explorer

This commit is contained in:
momo5502
2025-04-29 20:57:20 +02:00
parent 2fce53b3e7
commit da0cd03c57
9 changed files with 709 additions and 75 deletions

45
page/package-lock.json generated
View File

@@ -11,6 +11,7 @@
"@fontsource/inter": "^5.2.5",
"@irori/idbfs": "^0.5.0",
"@radix-ui/react-checkbox": "^1.2.3",
"@radix-ui/react-context-menu": "^2.2.12",
"@radix-ui/react-dialog": "^1.1.11",
"@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-label": "^2.1.4",
@@ -36,7 +37,8 @@
"react-window": "^1.8.11",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.4",
"tw-animate-css": "^1.2.8"
"tw-animate-css": "^1.2.8",
"vaul": "^1.1.2"
},
"devDependencies": {
"@eslint/js": "^9.22.0",
@@ -1356,6 +1358,34 @@
}
}
},
"node_modules/@radix-ui/react-context-menu": {
"version": "2.2.12",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.12.tgz",
"integrity": "sha512-5UFKuTMX8F2/KjHvyqu9IYT8bEtDSCJwwIx1PghBo4jh9S6jJVsceq9xIjqsOVcxsynGwV5eaqPE3n/Cu+DrSA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-menu": "2.1.12",
"@radix-ui/react-primitive": "2.1.0",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.11.tgz",
@@ -5265,6 +5295,19 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/vaul": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",
"integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-dialog": "^1.1.1"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/vite": {
"version": "6.3.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.3.tgz",

View File

@@ -13,6 +13,7 @@
"@fontsource/inter": "^5.2.5",
"@irori/idbfs": "^0.5.0",
"@radix-ui/react-checkbox": "^1.2.3",
"@radix-ui/react-context-menu": "^2.2.12",
"@radix-ui/react-dialog": "^1.1.11",
"@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-label": "^2.1.4",
@@ -38,7 +39,8 @@
"react-window": "^1.8.11",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.4",
"tw-animate-css": "^1.2.8"
"tw-animate-css": "^1.2.8",
"vaul": "^1.1.2"
},
"devDependencies": {
"@eslint/js": "^9.22.0",

View File

@@ -0,0 +1,79 @@
import React from "react";
import { Folder, FolderElement, FolderElementType } from "./components/folder";
import { Filesystem } from "./filesystem";
export interface FilesystemExplorerProps {
filesystem: Filesystem;
}
export interface FilesystemExplorerState {
path: string[];
}
function getFolderElements(filesystem: Filesystem, path: string[]) {
const fullPath = "/root/filesys/" + path.join("/");
const files = filesystem.readDir(fullPath);
return files
.filter((f) => {
if (f == ".") {
return false;
}
if (path.length == 0 && f == "..") {
return false;
}
return true;
})
.map((f) => {
const element: FolderElement = {
name: f,
type: filesystem.isFolder(`${fullPath}/${f}`)
? FolderElementType.Folder
: FolderElementType.File,
};
return element;
});
}
export class FilesystemExplorer extends React.Component<
FilesystemExplorerProps,
FilesystemExplorerState
> {
constructor(props: FilesystemExplorerProps) {
super(props);
this._onElementSelect = this._onElementSelect.bind(this);
this.state = {
path: [],
};
}
_onElementSelect(element: FolderElement) {
if (element.type != FolderElementType.Folder) {
return;
}
this.setState((s) => {
const path = [...s.path];
if (element.name == "..") {
path.pop();
} else {
path.push(element.name);
}
return {
path,
};
});
}
render() {
const elements = getFolderElements(this.props.filesystem, this.state.path);
return <Folder elements={elements} clickHandler={this._onElementSelect} />;
}
}

View File

@@ -4,7 +4,7 @@ import { Output } from "@/components/output";
import { Separator } from "@/components/ui/separator";
import { Emulator, UserFile, EmulationState } from "./emulator";
import { setupFilesystem } from "./filesystem";
import { Filesystem, setupFilesystem } from "./filesystem";
import "./App.css";
import {
@@ -38,6 +38,18 @@ import {
} 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");
@@ -75,8 +87,23 @@ export function Playground() {
const output = useRef<Output>(null);
const [settings, setSettings] = useState(createDefaultSettings());
const [emulator, setEmulator] = useState<Emulator | null>(null);
const [filesystem, setFilesystem] = useState<Filesystem | null>(null);
const [filesystemPromise, setFilesystemPromise] =
useState<Promise<Filesystem> | null>(null);
const [, forceUpdate] = useReducer((x) => x + 1, 0);
if (!filesystemPromise) {
const promise = new Promise<Filesystem>((resolve) => {
setupFilesystem((current, total, file) => {
logLine(`Processing filesystem (${current}/${total}): ${file}`);
}).then(resolve);
});
promise.then(setFilesystem);
setFilesystemPromise(promise);
}
function logLine(line: string) {
output.current?.logLine(line);
}
@@ -103,7 +130,7 @@ export function Playground() {
logLine("Starting emulation...");
await setupFilesystem((current, total, file) => {
const fs = await setupFilesystem((current, total, file) => {
logLine(`Processing filesystem (${current}/${total}): ${file}`);
});
@@ -111,7 +138,7 @@ export function Playground() {
new_emulator.onTerminate().then(() => setEmulator(null));
setEmulator(new_emulator);
new_emulator.start(settings, userFile);
new_emulator.start(settings, userFile, fs);
}
async function loadAndRunUserFile() {
@@ -186,6 +213,31 @@ export function Playground() {
<SettingsMenu settings={settings} onChange={setSettings} />
</PopoverContent>
</Popover>
{!filesystem ? (
<></>
) : (
<Drawer>
<DrawerTrigger asChild>
<Button size="sm" variant="secondary" className="fancy">
<GearFill /> Filesystem
</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
{/*<DrawerTitle>Filesystem Explorer</DrawerTitle>
<DrawerDescription>Description</DrawerDescription>
<DrawerClose asChild>
<Button variant="outline">Close</Button>
</DrawerClose>*/}
</DrawerHeader>
<DrawerFooter>
<FilesystemExplorer filesystem={filesystem} />
</DrawerFooter>
</DrawerContent>
</Drawer>
)}
<div className="text-right flex-1">
<StatusIndicator
state={emulator ? emulator.getState() : EmulationState.Stopped}

View File

@@ -0,0 +1,110 @@
import {
FolderFill,
FolderSymlinkFill,
FileEarmark,
} from "react-bootstrap-icons";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
export enum FolderElementType {
Folder = 0,
File,
}
export interface FolderElement {
name: string;
type: FolderElementType;
}
type ClickHandler = (element: FolderElement) => void;
export interface FolderProps {
elements: FolderElement[];
clickHandler: ClickHandler;
//deleteHandler: (element: FolderElement) => void;
//renameHandler: (element: FolderElement, name: string) => void;
}
function elementComparator(e1: FolderElement, e2: FolderElement) {
if (e1.type != e2.type) {
return e1.type - e2.type;
}
return e1.name.localeCompare(e2.name);
}
function renderIcon(element: FolderElement) {
let className = "w-10 h-10";
switch (element.type) {
case FolderElementType.File:
return <FileEarmark className={className} />;
case FolderElementType.Folder:
return element.name == ".." ? (
<FolderSymlinkFill className={className} />
) : (
<FolderFill className={className} />
);
default:
return <></>;
}
}
function renderElement(element: FolderElement, clickHandler: ClickHandler) {
return (
<div
key={`folder-element-${element.name}`}
onClick={() => clickHandler(element)}
className="folder-element select-none flex flex-col gap-4 items-center text-center p-4 m-4 w-30 h-25 rounded-lg border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50"
>
{renderIcon(element)}
<span className="whitespace-nowrap text-ellipsis overflow-hidden w-24">
{element.name}
</span>
</div>
);
}
function renderElementWithContext(
element: FolderElement,
clickHandler: ClickHandler,
) {
if (element.name == "..") {
return renderElement(element, clickHandler);
}
return (
<ContextMenu>
<ContextMenuTrigger>
{renderElement(element, clickHandler)}
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>Rename</ContextMenuItem>
<ContextMenuItem>Delete</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
}
export function Folder(props: FolderProps) {
return (
<ScrollArea className="h-[50dvh]">
<ContextMenu>
<ContextMenuTrigger>
<div className="folder flex flex-wrap">
{props.elements
.sort(elementComparator)
.map((e) => renderElementWithContext(e, props.clickHandler))}
</div>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>Create new folder</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</ScrollArea>
);
}

View File

@@ -0,0 +1,250 @@
import * as React from "react";
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function ContextMenu({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
}
function ContextMenuTrigger({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
return (
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
);
}
function ContextMenuGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
);
}
function ContextMenuPortal({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
);
}
function ContextMenuSub({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
}
function ContextMenuRadioGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
return (
<ContextMenuPrimitive.RadioGroup
data-slot="context-menu-radio-group"
{...props}
/>
);
}
function ContextMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.SubTrigger
data-slot="context-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</ContextMenuPrimitive.SubTrigger>
);
}
function ContextMenuSubContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
return (
<ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className,
)}
{...props}
/>
);
}
function ContextMenuContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
data-slot="context-menu-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className,
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
);
}
function ContextMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<ContextMenuPrimitive.Item
data-slot="context-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function ContextMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
return (
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
);
}
function ContextMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
return (
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
);
}
function ContextMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.Label
data-slot="context-menu-label"
data-inset={inset}
className={cn(
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className,
)}
{...props}
/>
);
}
function ContextMenuSeparator({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
return (
<ContextMenuPrimitive.Separator
data-slot="context-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function ContextMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="context-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
};

View File

@@ -0,0 +1,130 @@
import * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul";
import { cn } from "@/lib/utils";
function Drawer({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
}
function DrawerTrigger({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
}
function DrawerPortal({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
}
function DrawerClose({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
}
function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}
function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
className,
)}
{...props}
>
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
);
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
);
}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
);
}
function DrawerTitle({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
);
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
};

View File

@@ -5,7 +5,7 @@ import * as flatbuffers from "flatbuffers";
import * as fbDebugger from "@/fb/debugger";
import * as fbDebuggerEvent from "@/fb/debugger/event";
import { storeFile } from "./filesystem";
import { Filesystem } from "./filesystem";
type LogHandler = (lines: string[]) => void;
@@ -80,6 +80,7 @@ export class Emulator {
async start(
settings: Settings = createDefaultSettings(),
userFile: UserFile | null = null,
fs: Filesystem,
) {
var file = "c:/test-sample.exe";
if (userFile) {
@@ -87,7 +88,7 @@ export class Emulator {
const canonicalName = filename?.toLowerCase();
file = "c:/" + canonicalName;
await storeFile("root/filesys/c/" + canonicalName, userFile.data);
await fs.storeFile("root/filesys/c/" + canonicalName, userFile.data);
}
this._setState(EmulationState.Running);

View File

@@ -1,65 +1,6 @@
import { parseZipFile, FileEntry, ProgressHandler } from "./zip-file";
import { parseZipFile, ProgressHandler } from "./zip-file";
import idbfsModule, { MainModule } from "@irori/idbfs";
function openDatabase(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open("cacheDB", 1);
request.onerror = (event: Event) => {
reject(event);
};
request.onsuccess = (event: Event) => {
resolve((event as any).target.result as IDBDatabase);
};
request.onupgradeneeded = (event: Event) => {
const db = (event as any).target.result as IDBDatabase;
if (!db.objectStoreNames.contains("cacheStore")) {
db.createObjectStore("cacheStore", { keyPath: "id" });
}
};
});
}
async function saveData(id: string, data: any) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(["cacheStore"], "readwrite");
const objectStore = transaction.objectStore("cacheStore");
const request = objectStore.put({ id: id, data: data });
request.onsuccess = () => {
resolve("Data saved successfully");
};
request.onerror = (event) => {
reject("Save error: " + (event as any).target.errorCode);
};
});
}
async function getData(id: string) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(["cacheStore"], "readonly");
const objectStore = transaction.objectStore("cacheStore");
const request = objectStore.get(id);
request.onsuccess = (event) => {
if ((event as any).target.result) {
resolve((event as any).target.result.data);
} else {
resolve(null);
}
};
request.onerror = (event) => {
reject("Retrieve error: " + (event as any).target.errorCode);
};
});
}
function fetchFilesystemZip() {
return fetch("./root.zip?1", {
method: "GET",
@@ -97,11 +38,42 @@ async function initializeIDBFS() {
return idbfs;
}
export class Filesystem {
private idbfs: MainModule;
constructor(idbfs: MainModule) {
this.idbfs = idbfs;
}
async storeFile(file: string, data: ArrayBuffer) {
const buffer = new Uint8Array(data);
this.idbfs.FS.writeFile(file, buffer);
await this.sync();
}
async sync() {
await synchronizeIDBFS(this.idbfs, false);
}
readDir(dir: string): string[] {
return this.idbfs.FS.readdir(dir);
}
stat(file: string) {
return this.idbfs.FS.stat(file, false);
}
isFolder(file: string) {
return (this.stat(file).mode & 0x4000) != 0;
}
}
export async function setupFilesystem(progressHandler: ProgressHandler) {
const idbfs = await initializeIDBFS();
const fs = new Filesystem(idbfs);
if (idbfs.FS.analyzePath("/root/api-set.bin", false).exists) {
return;
return fs;
}
const filesystem = await fetchFilesystem(progressHandler);
@@ -119,12 +91,7 @@ export async function setupFilesystem(progressHandler: ProgressHandler) {
}
});
await synchronizeIDBFS(idbfs, false);
}
await fs.sync();
export async function storeFile(file: string, data: ArrayBuffer) {
const idbfs = await initializeIDBFS();
const buffer = new Uint8Array(data);
idbfs.FS.writeFile(file, buffer);
await synchronizeIDBFS(idbfs, false);
return fs;
}