From 0312b841d9b5fb1172a52c669ab597816e2977ae Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Sat, 5 Apr 2025 19:13:08 +0200 Subject: [PATCH] Implement FileTreeView component --- .../components/general/input/FileTreeView.tsx | 143 +++++++++++++----- .../general/modals/LibraryCreationModal.tsx | 8 +- gameyfin/src/main/frontend/views/TestView.tsx | 5 +- .../gameyfin/core/filesystem/FileDto.kt | 7 + .../gameyfin/core/filesystem/FileType.kt | 6 + .../core/filesystem/FilesystemEndpoint.kt | 14 ++ .../core/filesystem/FilesystemService.kt | 24 +++ gameyfin/vite.generated.ts | 2 +- 8 files changed, 167 insertions(+), 42 deletions(-) create mode 100644 gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/filesystem/FileDto.kt create mode 100644 gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/filesystem/FileType.kt create mode 100644 gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/filesystem/FilesystemEndpoint.kt create mode 100644 gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/filesystem/FilesystemService.kt diff --git a/gameyfin/src/main/frontend/components/general/input/FileTreeView.tsx b/gameyfin/src/main/frontend/components/general/input/FileTreeView.tsx index 4737dd2..71ec15c 100644 --- a/gameyfin/src/main/frontend/components/general/input/FileTreeView.tsx +++ b/gameyfin/src/main/frontend/components/general/input/FileTreeView.tsx @@ -1,61 +1,132 @@ -import TreeView, {flattenTree} from "react-accessible-treeview"; +import TreeView, {flattenTree, INode, NodeId} from "react-accessible-treeview"; import {File, Folder, FolderOpen, IconContext} from "@phosphor-icons/react"; +import {useEffect, useState} from "react"; +import {FilesystemEndpoint} from "Frontend/generated/endpoints"; +import FileDto from "Frontend/generated/de/grimsi/gameyfin/core/filesystem/FileDto"; +import FileType from "Frontend/generated/de/grimsi/gameyfin/core/filesystem/FileType"; +import {IFlatMetadata} from "react-accessible-treeview/dist/TreeView/utils"; -const folder = { - name: "", - children: [ - { - name: "src", - children: [{name: "index.js"}, {name: "styles.css"}], - }, - { - name: "node_modules", - children: [ - { - name: "react-accessible-treeview", - children: [{name: "index.js"}], - }, - {name: "react", children: [{name: "index.js"}]}, - ], - }, - { - name: ".npmignore", - }, - { - name: "package.json", - }, - { - name: "webpack.config.js", - }, - ], -}; +interface ITreeNode { + id?: NodeId; + name: string; + isBranch?: boolean; + children?: ITreeNode[]; + metadata?: M; +} -const data = flattenTree(folder); +export default function FileTreeView({setSelectedPath}: { setSelectedPath: (file: string) => void }) { + const rootNode: INode = { + id: "root", + name: "", + children: [], + parent: null + } + + const [fileTree, setFileTree] = useState(); + const [flattenedFileTree, setFlattenedFileTree] = useState([rootNode]); + + useEffect(() => { + FilesystemEndpoint.listSubDirectories(undefined).then( + result => { + if (result === undefined) return; + result = result.filter(r => r !== undefined); + const nodes = fileDtosToTree(result as FileDto[]); + const tree = flattenTree(nodes); + setFileTree(nodes); + setFlattenedFileTree(tree); + } + ) + }, []); + + function getAbsolutePath(node: INode, path: string = ""): string { + if (node.parent === null) { + return path ? `${node.name}/${path}` : node.name; + } + const parentNode = flattenedFileTree.find(n => n.id === node.parent); + if (!parentNode) { + throw new Error(`Parent node with id ${node.parent} not found`); + } + return getAbsolutePath(parentNode, `${node.name}/${path}`); + } + + async function onLoadData({element}: { element: INode }) { + const absolutePath = getAbsolutePath(element); + + let subDirectories = await FilesystemEndpoint.listSubDirectories(absolutePath); + if (subDirectories === undefined) return; + subDirectories = subDirectories.filter(r => r !== undefined); + + const newNodes = fileDtosToNodes(subDirectories as FileDto[]); + const updatedTree = updateTreeWithNewNodes(fileTree!!, element.id, newNodes); + + setFileTree(updatedTree); + setFlattenedFileTree(flattenTree(updatedTree)); + setSelectedPath(absolutePath); + } + + function updateTreeWithNewNodes(tree: ITreeNode, nodeId: NodeId, newNodes: ITreeNode[]): ITreeNode { + if (tree.id === nodeId) { + return {...tree, children: newNodes}; + } + + if (tree.children) { + return { + ...tree, + children: tree.children.map(child => updateTreeWithNewNodes(child, nodeId, newNodes)) + }; + } + + return tree; + } + + function fileDtosToTree(fileDtos: FileDto[], parent: (INode | null) = null): ITreeNode { + const nodes = fileDtosToNodes(fileDtos); + + if (parent === null) { + return {...rootNode, children: nodes}; + } + + return {...parent, children: nodes}; + } + + function fileDtosToNodes(fileDtos: FileDto[]): ITreeNode[] { + return fileDtos.map(fileDto => ({ + id: fileDto.hash, + name: fileDto.name || "", + isBranch: fileDto.type === FileType.DIRECTORY, + children: [] + })); + } -export default function FileTreeView({}) { return ( -
+
( -
{isBranch ? : } {element.name}
)} - className="w-full" /> -
{JSON.stringify(data, null, 2)}
+
{JSON.stringify(flattenedFileTree, null, 2)}
); } diff --git a/gameyfin/src/main/frontend/components/general/modals/LibraryCreationModal.tsx b/gameyfin/src/main/frontend/components/general/modals/LibraryCreationModal.tsx index 7759a57..51472af 100644 --- a/gameyfin/src/main/frontend/components/general/modals/LibraryCreationModal.tsx +++ b/gameyfin/src/main/frontend/components/general/modals/LibraryCreationModal.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, {useState} from "react"; import {addToast, Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader} from "@heroui/react"; import {Form, Formik} from "formik"; import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/LibraryDto"; @@ -19,6 +19,8 @@ export default function LibraryCreationModal({ isOpen, onOpenChange }: LibraryCreationModalProps) { + const [selectedPath, setSelectedPath] = useState(""); + async function createLibrary(library: LibraryDto) { try { const newLibrary = await LibraryEndpoint.createLibrary(library as LibraryDto); @@ -71,8 +73,8 @@ export default function LibraryCreationModal({ required />
- - +
{selectedPath}
+