mirror of
https://github.com/momo5502/emulator.git
synced 2026-01-11 16:46:16 +00:00
Basic working file explorer
This commit is contained in:
@@ -4,6 +4,7 @@ import { Filesystem } from "./filesystem";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "./components/ui/button";
|
||||
import { Input } from "./components/ui/input";
|
||||
import { DialogDescription } from "@radix-ui/react-dialog";
|
||||
|
||||
export interface FilesystemExplorerProps {
|
||||
filesystem: Filesystem;
|
||||
@@ -19,10 +21,34 @@ export interface FilesystemExplorerState {
|
||||
path: string[];
|
||||
createFolder: boolean;
|
||||
errorText: string;
|
||||
removeFile: string;
|
||||
renameFile: string;
|
||||
}
|
||||
|
||||
function makeFullPath(path: string[]) {
|
||||
return "/root/filesys/" + path.join("/");
|
||||
}
|
||||
|
||||
function makeFullPathAndJoin(path: string[], element: string) {
|
||||
return makeFullPath([...path, element]);
|
||||
}
|
||||
|
||||
function makeFullPathWithState(
|
||||
state: FilesystemExplorerState,
|
||||
element: string,
|
||||
) {
|
||||
return makeFullPathAndJoin(state.path, element);
|
||||
}
|
||||
|
||||
function makeRelativePathWithState(
|
||||
state: FilesystemExplorerState,
|
||||
element: string,
|
||||
) {
|
||||
return [...state.path, element].join("/");
|
||||
}
|
||||
|
||||
function getFolderElements(filesystem: Filesystem, path: string[]) {
|
||||
const fullPath = "/root/filesys/" + path.join("/");
|
||||
const fullPath = makeFullPath(path);
|
||||
const files = filesystem.readDir(fullPath);
|
||||
|
||||
return files
|
||||
@@ -56,15 +82,14 @@ 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: "",
|
||||
removeFile: "",
|
||||
renameFile: "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -92,16 +117,18 @@ export class FilesystemExplorer extends React.Component<
|
||||
});
|
||||
}
|
||||
|
||||
_showFolderCreateDialog() {
|
||||
this.setState({
|
||||
createFolder: true,
|
||||
});
|
||||
async _onFileRename(file: string, newFile: string) {
|
||||
const oldPath = makeFullPathWithState(this.state, file);
|
||||
const newPath = makeFullPathWithState(this.state, newFile);
|
||||
|
||||
this.setState({ renameFile: "" });
|
||||
|
||||
await this.props.filesystem.rename(oldPath, newPath);
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
async _onFolderCreate(name: string) {
|
||||
this.setState({
|
||||
createFolder: false,
|
||||
});
|
||||
this.setState({ createFolder: false });
|
||||
|
||||
name = name.toLowerCase();
|
||||
|
||||
@@ -110,75 +137,185 @@ export class FilesystemExplorer extends React.Component<
|
||||
}
|
||||
|
||||
if (name.includes("/") || name.includes("\\")) {
|
||||
this._showError("Folder must not contain special characters");
|
||||
return;
|
||||
}
|
||||
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("/");
|
||||
const fullPath = makeFullPathWithState(this.state, name);
|
||||
await this.props.filesystem.createFolder(fullPath);
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
_renderCreateFolderDialog() {
|
||||
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>
|
||||
<DialogDescription className="hidden">
|
||||
Create new folder
|
||||
</DialogDescription>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
_renderRenameDialog() {
|
||||
return (
|
||||
<Dialog
|
||||
open={this.state.renameFile.length > 0}
|
||||
onOpenChange={(open) => (open ? {} : this.setState({ renameFile: "" }))}
|
||||
>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
const newName = (e.target as any).elements.name.value;
|
||||
this._onFileRename(this.state.renameFile, newName);
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Rename {this.state.renameFile}</DialogTitle>
|
||||
<DialogDescription className="hidden">
|
||||
Rename {this.state.renameFile}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<Input id="name" defaultValue={this.state.renameFile} />
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="submit" className="fancy rounded-lg">
|
||||
Rename
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button variant="secondary" className="fancy rounded-lg">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
_renderErrorDialog() {
|
||||
return (
|
||||
<Dialog
|
||||
open={this.state.errorText.length > 0}
|
||||
onOpenChange={(open) => (open ? {} : this.setState({ errorText: "" }))}
|
||||
>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Error</DialogTitle>
|
||||
<DialogDescription className="hidden">
|
||||
Error: {this.state.errorText}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">{this.state.errorText}</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => this.setState({ errorText: "" })}
|
||||
>
|
||||
Ok
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
_renderRemoveDialog() {
|
||||
return (
|
||||
<Dialog
|
||||
open={this.state.removeFile.length > 0}
|
||||
onOpenChange={(open) => (open ? {} : this.setState({ removeFile: "" }))}
|
||||
>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete {this.state.removeFile}?</DialogTitle>
|
||||
<DialogDescription className="hidden">
|
||||
Delete {this.state.removeFile}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
Are you sure you want to delete{" "}
|
||||
<b>
|
||||
{makeRelativePathWithState(this.state, this.state.removeFile)}
|
||||
</b>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
const file = makeFullPathWithState(
|
||||
this.state,
|
||||
this.state.removeFile,
|
||||
);
|
||||
this.setState({ removeFile: "" });
|
||||
this.props.filesystem
|
||||
.unlink(file)
|
||||
.then(() => this.forceUpdate());
|
||||
}}
|
||||
>
|
||||
Ok
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
this.setState({ removeFile: "" });
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const elements = getFolderElements(this.props.filesystem, this.state.path);
|
||||
|
||||
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>
|
||||
{this._renderCreateFolderDialog()}
|
||||
{this._renderRenameDialog()}
|
||||
{this._renderErrorDialog()}
|
||||
{this._renderRemoveDialog()}
|
||||
|
||||
<Folder
|
||||
elements={elements}
|
||||
clickHandler={this._onElementSelect}
|
||||
createFolderHandler={this._showFolderCreateDialog}
|
||||
createFolderHandler={() => this.setState({ createFolder: true })}
|
||||
removeElementHandler={(e) => this.setState({ removeFile: e.name })}
|
||||
renameElementHandler={(e) => this.setState({ renameFile: e.name })}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -225,11 +225,12 @@ export function Playground() {
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
{/*<DrawerTitle>Filesystem Explorer</DrawerTitle>
|
||||
<DrawerDescription>Description</DrawerDescription>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</DrawerClose>*/}
|
||||
<DrawerTitle className="hidden">
|
||||
Filesystem Explorer
|
||||
</DrawerTitle>
|
||||
<DrawerDescription className="hidden">
|
||||
Filesystem Explorer
|
||||
</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<DrawerFooter>
|
||||
<FilesystemExplorer filesystem={filesystem} />
|
||||
|
||||
@@ -33,13 +33,15 @@ export interface FolderElement {
|
||||
|
||||
type ClickHandler = (element: FolderElement) => void;
|
||||
type CreateFolderHandler = () => void;
|
||||
type RemoveElementHandler = (element: FolderElement) => void;
|
||||
type RenameElementHandler = (element: FolderElement) => void;
|
||||
|
||||
export interface FolderProps {
|
||||
elements: FolderElement[];
|
||||
clickHandler: ClickHandler;
|
||||
createFolderHandler: CreateFolderHandler;
|
||||
//deleteHandler: (element: FolderElement) => void;
|
||||
//renameHandler: (element: FolderElement, name: string) => void;
|
||||
removeElementHandler: RemoveElementHandler;
|
||||
renameElementHandler: RenameElementHandler;
|
||||
}
|
||||
|
||||
function elementComparator(e1: FolderElement, e2: FolderElement) {
|
||||
@@ -90,12 +92,9 @@ function renderElement(element: FolderElement, clickHandler: ClickHandler) {
|
||||
);
|
||||
}
|
||||
|
||||
function renderElementWithContext(
|
||||
element: FolderElement,
|
||||
clickHandler: ClickHandler,
|
||||
) {
|
||||
function renderElementWithContext(element: FolderElement, props: FolderProps) {
|
||||
if (element.name == "..") {
|
||||
return renderElement(element, clickHandler);
|
||||
return renderElement(element, props.clickHandler);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -103,7 +102,7 @@ function renderElementWithContext(
|
||||
<ContextMenuTrigger>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
{renderElement(element, clickHandler)}
|
||||
{renderElement(element, props.clickHandler)}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{element.name}</p>
|
||||
@@ -113,18 +112,23 @@ function renderElementWithContext(
|
||||
<ContextMenuContent>
|
||||
<ContextMenuLabel inset>{element.name}</ContextMenuLabel>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem>Rename</ContextMenuItem>
|
||||
<ContextMenuItem>Delete</ContextMenuItem>
|
||||
<ContextMenuItem onClick={() => props.renameElementHandler(element)}>
|
||||
Rename
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={() => props.removeElementHandler(element)}>
|
||||
Delete
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
function renderElementWrapper( element: FolderElement,
|
||||
clickHandler: ClickHandler,) {
|
||||
return <div key={`folder-element-${element.name}`}>
|
||||
{renderElementWithContext(element, clickHandler)}
|
||||
function renderElementWrapper(element: FolderElement, props: FolderProps) {
|
||||
return (
|
||||
<div key={`folder-element-${element.name}`}>
|
||||
{renderElementWithContext(element, props)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderFolderCreator(createFolderHandler: CreateFolderHandler) {
|
||||
@@ -144,7 +148,7 @@ export function Folder(props: FolderProps) {
|
||||
<div className="folder flex flex-wrap">
|
||||
{props.elements
|
||||
.sort(elementComparator)
|
||||
.map((e) => renderElementWrapper(e, props.clickHandler))}
|
||||
.map((e) => renderElementWrapper(e, props))}
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
|
||||
@@ -51,6 +51,31 @@ export class Filesystem {
|
||||
await this.sync();
|
||||
}
|
||||
|
||||
_unlinkRecursive(element: string) {
|
||||
if (!this.isFolder(element)) {
|
||||
this.idbfs.FS.unlink(element);
|
||||
return;
|
||||
}
|
||||
|
||||
this.readDir(element) //
|
||||
.filter((e) => e != "." && e != "..")
|
||||
.forEach((e) => {
|
||||
this._unlinkRecursive(`${element}/${e}`);
|
||||
});
|
||||
|
||||
this.idbfs.FS.rmdir(element);
|
||||
}
|
||||
|
||||
async rename(oldFile: string, newFile: string) {
|
||||
this.idbfs.FS.rename(oldFile, newFile);
|
||||
await this.sync();
|
||||
}
|
||||
|
||||
async unlink(file: string) {
|
||||
this._unlinkRecursive(file);
|
||||
await this.sync();
|
||||
}
|
||||
|
||||
async createFolder(folder: string) {
|
||||
this.idbfs.FS.mkdir(folder, 777);
|
||||
await this.sync();
|
||||
|
||||
Reference in New Issue
Block a user