mirror of
https://github.com/momo5502/emulator.git
synced 2026-01-21 20:53:56 +00:00
Add dialogs
This commit is contained in:
@@ -2,11 +2,23 @@ import React from "react";
|
||||
import { Folder, FolderElement, FolderElementType } from "./components/folder";
|
||||
import { Filesystem } from "./filesystem";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "./components/ui/button";
|
||||
import { Input } from "./components/ui/input";
|
||||
|
||||
export interface FilesystemExplorerProps {
|
||||
filesystem: Filesystem;
|
||||
}
|
||||
export interface FilesystemExplorerState {
|
||||
path: string[];
|
||||
createFolder: boolean;
|
||||
errorText: string;
|
||||
}
|
||||
|
||||
function getFolderElements(filesystem: Filesystem, path: string[]) {
|
||||
@@ -44,13 +56,22 @@ export class FilesystemExplorer extends React.Component<
|
||||
constructor(props: FilesystemExplorerProps) {
|
||||
super(props);
|
||||
|
||||
this._showError = this._showError.bind(this);
|
||||
this._onFolderCreate = this._onFolderCreate.bind(this);
|
||||
this._onElementSelect = this._onElementSelect.bind(this);
|
||||
this._showFolderCreateDialog = this._showFolderCreateDialog.bind(this);
|
||||
|
||||
this.state = {
|
||||
path: [],
|
||||
createFolder: false,
|
||||
errorText: "",
|
||||
};
|
||||
}
|
||||
|
||||
_showError(errorText: string) {
|
||||
this.setState({ errorText });
|
||||
}
|
||||
|
||||
_onElementSelect(element: FolderElement) {
|
||||
if (element.type != FolderElementType.Folder) {
|
||||
return;
|
||||
@@ -71,9 +92,95 @@ export class FilesystemExplorer extends React.Component<
|
||||
});
|
||||
}
|
||||
|
||||
_showFolderCreateDialog() {
|
||||
this.setState({
|
||||
createFolder: true,
|
||||
});
|
||||
}
|
||||
|
||||
async _onFolderCreate(name: string) {
|
||||
this.setState({
|
||||
createFolder: false,
|
||||
});
|
||||
|
||||
name = name.toLowerCase();
|
||||
|
||||
if (name.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.includes("/") || name.includes("\\")) {
|
||||
this._showError("Folder must not contain special characters");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.path.length == 0 && name.length > 1) {
|
||||
this._showError("Drives must be a single letter");
|
||||
return;
|
||||
}
|
||||
|
||||
const fullPath = "/root/filesys/" + [...this.state.path, name].join("/");
|
||||
await this.props.filesystem.createFolder(fullPath);
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
render() {
|
||||
const elements = getFolderElements(this.props.filesystem, this.state.path);
|
||||
|
||||
return <Folder elements={elements} clickHandler={this._onElementSelect} />;
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
open={this.state.createFolder}
|
||||
onOpenChange={(open) => this.setState({ createFolder: open })}
|
||||
>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
const folderName = (e.target as any).elements.name.value;
|
||||
this._onFolderCreate(folderName);
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create new folder</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<Input id="name" defaultValue="New Folder" />
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="submit" className="fancy rounded-lg">
|
||||
Create
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
open={this.state.errorText.length > 0}
|
||||
onOpenChange={(open) =>
|
||||
open ? {} : this.setState({ errorText: "" })
|
||||
}
|
||||
>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Error</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4">{this.state.errorText}</div>
|
||||
<DialogFooter>
|
||||
<Button variant="destructive" onClick={() => this.setState({ errorText: "" })}>
|
||||
Ok
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Folder
|
||||
elements={elements}
|
||||
clickHandler={this._onElementSelect}
|
||||
createFolderHandler={this._showFolderCreateDialog}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,10 +32,12 @@ export interface FolderElement {
|
||||
}
|
||||
|
||||
type ClickHandler = (element: FolderElement) => void;
|
||||
type CreateFolderHandler = () => void;
|
||||
|
||||
export interface FolderProps {
|
||||
elements: FolderElement[];
|
||||
clickHandler: ClickHandler;
|
||||
createFolderHandler: CreateFolderHandler;
|
||||
//deleteHandler: (element: FolderElement) => void;
|
||||
//renameHandler: (element: FolderElement, name: string) => void;
|
||||
}
|
||||
@@ -70,19 +72,18 @@ function getIcon(element: FolderElement, className: string = "") {
|
||||
}
|
||||
|
||||
function renderIcon(element: FolderElement) {
|
||||
let className = "w-10 h-10";
|
||||
let className = "w-6 h-6 flex-1";
|
||||
return getIcon(element, className);
|
||||
}
|
||||
|
||||
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"
|
||||
className="folder-element select-none flex flex-col gap-2 items-center text-center text-xs p-2 m-2 w-25 h-18 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">
|
||||
<span className="whitespace-nowrap text-ellipsis overflow-hidden w-20">
|
||||
{element.name}
|
||||
</span>
|
||||
</div>
|
||||
@@ -119,6 +120,21 @@ function renderElementWithContext(
|
||||
);
|
||||
}
|
||||
|
||||
function renderElementWrapper( element: FolderElement,
|
||||
clickHandler: ClickHandler,) {
|
||||
return <div key={`folder-element-${element.name}`}>
|
||||
{renderElementWithContext(element, clickHandler)}
|
||||
</div>
|
||||
}
|
||||
|
||||
function renderFolderCreator(createFolderHandler: CreateFolderHandler) {
|
||||
return (
|
||||
<ContextMenuItem onClick={createFolderHandler}>
|
||||
Create new Folder
|
||||
</ContextMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function Folder(props: FolderProps) {
|
||||
return (
|
||||
<ScrollArea className="h-[50dvh]">
|
||||
@@ -128,11 +144,11 @@ export function Folder(props: FolderProps) {
|
||||
<div className="folder flex flex-wrap">
|
||||
{props.elements
|
||||
.sort(elementComparator)
|
||||
.map((e) => renderElementWithContext(e, props.clickHandler))}
|
||||
.map((e) => renderElementWrapper(e, props.clickHandler))}
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem>Create new folder</ContextMenuItem>
|
||||
{renderFolderCreator(props.createFolderHandler)}
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
</TooltipProvider>
|
||||
|
||||
133
page/src/components/ui/dialog.tsx
Normal file
133
page/src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-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 DialogContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
@@ -45,12 +45,17 @@ export class Filesystem {
|
||||
this.idbfs = idbfs;
|
||||
}
|
||||
|
||||
async storeFile(file: string, data: ArrayBuffer) {
|
||||
async storeFiles(file: string, data: ArrayBuffer) {
|
||||
const buffer = new Uint8Array(data);
|
||||
this.idbfs.FS.writeFile(file, buffer);
|
||||
await this.sync();
|
||||
}
|
||||
|
||||
async createFolder(folder: string) {
|
||||
this.idbfs.FS.mkdir(folder, 777);
|
||||
await this.sync();
|
||||
}
|
||||
|
||||
async sync() {
|
||||
await synchronizeIDBFS(this.idbfs, false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user