Add dialogs

This commit is contained in:
momo5502
2025-04-30 08:06:28 +02:00
parent 3531d1d5ea
commit af5c2a9d5b
4 changed files with 269 additions and 8 deletions

View File

@@ -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}
/>
</>
);
}
}

View File

@@ -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>

View 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,
};

View File

@@ -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);
}