mirror of
https://github.com/momo5502/emulator.git
synced 2026-01-11 16:46:16 +00:00
Prepare filesystem explorer
This commit is contained in:
45
page/package-lock.json
generated
45
page/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
79
page/src/FilesystemExplorer.tsx
Normal file
79
page/src/FilesystemExplorer.tsx
Normal 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} />;
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
110
page/src/components/folder.tsx
Normal file
110
page/src/components/folder.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
250
page/src/components/ui/context-menu.tsx
Normal file
250
page/src/components/ui/context-menu.tsx
Normal 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,
|
||||
};
|
||||
130
page/src/components/ui/drawer.tsx
Normal file
130
page/src/components/ui/drawer.tsx
Normal 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,
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user