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

View File

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

View File

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

View File

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