Basic working file explorer

This commit is contained in:
momo5502
2025-04-30 08:59:25 +02:00
parent af5c2a9d5b
commit 51971c5ec7
4 changed files with 248 additions and 81 deletions

View File

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