mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-17 08:15:44 +00:00
Implement FileTreeView component
This commit is contained in:
@@ -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 {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 = {
|
interface ITreeNode<M extends IFlatMetadata = IFlatMetadata> {
|
||||||
|
id?: NodeId;
|
||||||
|
name: string;
|
||||||
|
isBranch?: boolean;
|
||||||
|
children?: ITreeNode<M>[];
|
||||||
|
metadata?: M;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FileTreeView({setSelectedPath}: { setSelectedPath: (file: string) => void }) {
|
||||||
|
const rootNode: INode = {
|
||||||
|
id: "root",
|
||||||
name: "",
|
name: "",
|
||||||
children: [
|
children: [],
|
||||||
{
|
parent: null
|
||||||
name: "src",
|
}
|
||||||
children: [{name: "index.js"}, {name: "styles.css"}],
|
|
||||||
},
|
const [fileTree, setFileTree] = useState<ITreeNode>();
|
||||||
{
|
const [flattenedFileTree, setFlattenedFileTree] = useState<INode[]>([rootNode]);
|
||||||
name: "node_modules",
|
|
||||||
children: [
|
useEffect(() => {
|
||||||
{
|
FilesystemEndpoint.listSubDirectories(undefined).then(
|
||||||
name: "react-accessible-treeview",
|
result => {
|
||||||
children: [{name: "index.js"}],
|
if (result === undefined) return;
|
||||||
},
|
result = result.filter(r => r !== undefined);
|
||||||
{name: "react", children: [{name: "index.js"}]},
|
const nodes = fileDtosToTree(result as FileDto[]);
|
||||||
],
|
const tree = flattenTree(nodes);
|
||||||
},
|
setFileTree(nodes);
|
||||||
{
|
setFlattenedFileTree(tree);
|
||||||
name: ".npmignore",
|
}
|
||||||
},
|
)
|
||||||
{
|
}, []);
|
||||||
name: "package.json",
|
|
||||||
},
|
function getAbsolutePath(node: INode, path: string = ""): string {
|
||||||
{
|
if (node.parent === null) {
|
||||||
name: "webpack.config.js",
|
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))
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const data = flattenTree(folder);
|
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 (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4 bg-contrast">
|
||||||
<TreeView
|
<TreeView
|
||||||
data={data}
|
data={flattenedFileTree}
|
||||||
aria-label="directory tree"
|
aria-label="directory tree"
|
||||||
|
onLoadData={onLoadData}
|
||||||
nodeRenderer={({
|
nodeRenderer={({
|
||||||
element,
|
element,
|
||||||
isBranch,
|
isBranch,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
|
isSelected,
|
||||||
getNodeProps,
|
getNodeProps,
|
||||||
level,
|
level,
|
||||||
}) => (
|
}) => (
|
||||||
<IconContext.Provider value={{size: 32, weight: "regular"}}>
|
<IconContext.Provider value={{size: 32, weight: "regular"}}>
|
||||||
<div {...getNodeProps()} className="flex flex-row items-center gap-2"
|
<div {...getNodeProps()}
|
||||||
|
className={`
|
||||||
|
flex flex-row items-center gap-2
|
||||||
|
rounded-md cursor-pointer
|
||||||
|
${isSelected ? 'bg-primary' : 'hover:bg-primary/20'}`
|
||||||
|
}
|
||||||
style={{paddingLeft: 10 * (level - 1)}}>
|
style={{paddingLeft: 10 * (level - 1)}}>
|
||||||
{isBranch ? <FolderIcon isOpen={isExpanded}/> : <FileIcon fileName={element.name}/>}
|
{isBranch ? <FolderIcon isOpen={isExpanded}/> : <FileIcon fileName={element.name}/>}
|
||||||
{element.name}
|
{element.name}
|
||||||
</div>
|
</div>
|
||||||
</IconContext.Provider>
|
</IconContext.Provider>
|
||||||
)}
|
)}
|
||||||
className="w-full"
|
|
||||||
/>
|
/>
|
||||||
<pre>{JSON.stringify(data, null, 2)}</pre>
|
<pre>{JSON.stringify(flattenedFileTree, null, 2)}</pre>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {addToast, Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader} from "@heroui/react";
|
||||||
import {Form, Formik} from "formik";
|
import {Form, Formik} from "formik";
|
||||||
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/LibraryDto";
|
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/LibraryDto";
|
||||||
@@ -19,6 +19,8 @@ export default function LibraryCreationModal({
|
|||||||
isOpen,
|
isOpen,
|
||||||
onOpenChange
|
onOpenChange
|
||||||
}: LibraryCreationModalProps) {
|
}: LibraryCreationModalProps) {
|
||||||
|
const [selectedPath, setSelectedPath] = useState("");
|
||||||
|
|
||||||
async function createLibrary(library: LibraryDto) {
|
async function createLibrary(library: LibraryDto) {
|
||||||
try {
|
try {
|
||||||
const newLibrary = await LibraryEndpoint.createLibrary(library as LibraryDto);
|
const newLibrary = await LibraryEndpoint.createLibrary(library as LibraryDto);
|
||||||
@@ -71,8 +73,8 @@ export default function LibraryCreationModal({
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<pre>{selectedPath}</pre>
|
||||||
<FileTreeView/>
|
<FileTreeView setSelectedPath={setSelectedPath}/>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button variant="light" onPress={onClose}>
|
<Button variant="light" onPress={onClose}>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {GameOverviewCard} from "Frontend/components/general/cards/GameOverviewCa
|
|||||||
import FileTreeView from "Frontend/components/general/input/FileTreeView";
|
import FileTreeView from "Frontend/components/general/input/FileTreeView";
|
||||||
|
|
||||||
export default function TestView() {
|
export default function TestView() {
|
||||||
|
const [selectedPath, setSelectedPath] = useState("");
|
||||||
const [gameTitle, setGameTitle] = useState("");
|
const [gameTitle, setGameTitle] = useState("");
|
||||||
const [game, setGame] = useState<GameDto>();
|
const [game, setGame] = useState<GameDto>();
|
||||||
|
|
||||||
@@ -73,8 +74,8 @@ export default function TestView() {
|
|||||||
</div>
|
</div>
|
||||||
{game && <GameOverviewCard game={game}></GameOverviewCard>}
|
{game && <GameOverviewCard game={game}></GameOverviewCard>}
|
||||||
{game && <>{JSON.stringify(game, null, 2)}</>}
|
{game && <>{JSON.stringify(game, null, 2)}</>}
|
||||||
|
<pre>{selectedPath}</pre>
|
||||||
<FileTreeView/>
|
<FileTreeView setSelectedPath={setSelectedPath}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package de.grimsi.gameyfin.core.filesystem
|
||||||
|
|
||||||
|
data class FileDto(
|
||||||
|
val name: String,
|
||||||
|
val type: FileType,
|
||||||
|
val hash: Int
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package de.grimsi.gameyfin.core.filesystem
|
||||||
|
|
||||||
|
enum class FileType {
|
||||||
|
FILE,
|
||||||
|
DIRECTORY,
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package de.grimsi.gameyfin.core.filesystem
|
||||||
|
|
||||||
|
import com.vaadin.hilla.Endpoint
|
||||||
|
import de.grimsi.gameyfin.core.Role
|
||||||
|
import jakarta.annotation.security.RolesAllowed
|
||||||
|
|
||||||
|
@Endpoint
|
||||||
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
|
class FilesystemEndpoint {
|
||||||
|
|
||||||
|
fun listContents(path: String?) = FilesystemService().listContents(path)
|
||||||
|
|
||||||
|
fun listSubDirectories(path: String?) = FilesystemService().listSubDirectories(path)
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package de.grimsi.gameyfin.core.filesystem
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class FilesystemService {
|
||||||
|
/**
|
||||||
|
* Lists all files and directories in the given path.
|
||||||
|
* If the path is null or empty, it lists all root directories.
|
||||||
|
*
|
||||||
|
* @param path The path to list files and directories from.
|
||||||
|
* @return A list of FileDto objects representing the files and directories.
|
||||||
|
*/
|
||||||
|
fun listContents(path: String?): List<FileDto> {
|
||||||
|
val file = if (path.isNullOrEmpty()) File.listRoots().toList() else listOf(File(path))
|
||||||
|
return file.flatMap { it.listFiles()?.toList() ?: emptyList() }
|
||||||
|
.map { FileDto(it.name, if (it.isDirectory) FileType.DIRECTORY else FileType.FILE, it.hashCode()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun listSubDirectories(path: String?): List<FileDto> {
|
||||||
|
return listContents(path).filter { it.type == FileType.DIRECTORY }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -274,7 +274,7 @@ function statsExtracterPlugin(): PluginOption {
|
|||||||
const frontendFiles: Record<string, string> = {};
|
const frontendFiles: Record<string, string> = {};
|
||||||
frontendFiles['index.html'] = createHash('sha256').update(customIndexData.replace(/\r\n/g, '\n'), 'utf8').digest('hex');
|
frontendFiles['index.html'] = createHash('sha256').update(customIndexData.replace(/\r\n/g, '\n'), 'utf8').digest('hex');
|
||||||
|
|
||||||
const projectFileExtensions = ['.js', '.js.map', '.ts', '.ts.map', '.tsx', '.tsx.map', '.css', '.css.map'];
|
const projectFileExtensions = ['.js', '.js.map', '.ts', '.ts.map', '.tsx', '.tsx.map', '.css', '.css.map', '.'];
|
||||||
|
|
||||||
const isThemeComponentsResource = (id: string) =>
|
const isThemeComponentsResource = (id: string) =>
|
||||||
id.startsWith(themeOptions.frontendGeneratedFolder.replace(/\\/g, '/'))
|
id.startsWith(themeOptions.frontendGeneratedFolder.replace(/\\/g, '/'))
|
||||||
|
|||||||
Reference in New Issue
Block a user