Skip to content

Commit

Permalink
✨ ディレクトリーツリー
Browse files Browse the repository at this point in the history
  • Loading branch information
wappon28dev committed Aug 3, 2024
1 parent d55a76f commit 3939a70
Show file tree
Hide file tree
Showing 13 changed files with 187 additions and 109 deletions.
Binary file modified bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use routes::files::show_files;
fn main() {
let invoke_handler = {
let builder =
tauri_specta::ts::builder().commands(tauri_specta::collect_commands![show_files,]);
tauri_specta::ts::builder().commands(tauri_specta::collect_commands![show_files]);

#[cfg(debug_assertions)]
let builder = builder.path("../src/types/bindings.ts");
Expand Down
44 changes: 33 additions & 11 deletions src-tauri/src/routes/files.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,49 @@
use std::path::Path;

use serde::Serialize;
use specta::Type;

#[derive(Serialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct FileEntry {
pub base_path: String,
pub name: String,
pub is_dir: bool,
pub children: Option<Vec<FileEntry>>,
}

#[tauri::command]
#[specta::specta]
pub fn show_files(input: String) -> Vec<FileEntry> {
let path = std::path::Path::new(&input);
println!("Reading files from: {:?}", path);
fn read_directory(input: String, depth: usize) -> Vec<FileEntry> {
let target_path = Path::new(&input);

return std::fs::read_dir(path)
std::fs::read_dir(target_path)
.unwrap()
.map(|entry| {
let entry = entry.unwrap();
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
let is_dir = path.is_dir();
let name = entry.file_name().into_string().unwrap();
let name = entry.file_name().into_string().ok()?;

let children = if is_dir && depth < 3 {
Some(read_directory(path.to_str()?.to_string(), depth + 1))
} else {
None
};

FileEntry { name, is_dir }
Some(FileEntry {
base_path: input.clone(),
name,
is_dir,
children,
})
})
.collect();
.collect()
}

#[tauri::command]
#[specta::specta]
pub fn show_files(input: String) -> Vec<FileEntry> {
let target_path = Path::new(&input);
println!("Reading files from: {:?}", target_path);

return read_directory(input, 0);
}
65 changes: 47 additions & 18 deletions src/components/FileTree.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,45 @@
import { Icon } from "@iconify/react";
import { HStack, styled as p, VStack } from "panda/jsx";
import { type ReactElement } from "react";
import { type NodeRendererProps, Tree } from "react-arborist";
import { useEffect, useState, type ReactElement } from "react";
import { type NodeApi, type NodeRendererProps, Tree } from "react-arborist";
import { getIconUrlByName, getIconUrlForFilePath } from "vscode-material-icons";
import { processFileEntries } from "@/lib/utils/file";
import { type FileEntry } from "@/types/bindings";
import { type FileTreeData } from "@/types/file";

function Node({
node,
style,
dragHandle,
}: NodeRendererProps<FileEntry>): ReactElement {
}: NodeRendererProps<FileTreeData>): ReactElement {
const ICONS_URL = "/assets/material-icons";
const icon = getIconUrlForFilePath(node.data.name, ICONS_URL);
const folderIcon = getIconUrlByName("folder", ICONS_URL);

return (
<HStack
ref={dragHandle}
_hover={{
bg: "gray.100",
}}
onClick={() => {
node.toggle();
}}
style={style}
w="100%"
>
<p.div
style={{
opacity: node.data.isDir ? 1 : 0,
rotate: node.isOpen ? "0deg" : "-90deg",
}}
>
<Icon icon="mdi:triangle-small-down" />
</p.div>
<p.img
alt=""
height="1em"
src={node.data.is_dir ? folderIcon : icon}
src={node.data.isDir ? folderIcon : icon}
width="auto"
/>
<p.code overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">
Expand All @@ -39,21 +54,35 @@ export function FileTree({
}: {
fileEntries: FileEntry[];
}): ReactElement {
const [treeData, setTreeData] = useState<FileTreeData[]>([]);
const [activatedNode, setActivatedNode] = useState<NodeApi<FileTreeData>>();

useEffect(() => {
void processFileEntries(fileEntries).then((e) => {
setTreeData(e);
});
}, [fileEntries]);

useEffect(() => {
if (activatedNode == null) return;
const isTooDeep =
activatedNode.data.isDir && activatedNode.data.children == null;
console.log("too deep", isTooDeep);
}, [activatedNode]);

return (
<VStack w="100%">
<p>{fileEntries?.length}</p>
{fileEntries?.length !== 0 && (
<Tree
idAccessor={(d) => d.name}
indent={24}
initialData={fileEntries}
onToggle={(node) => {
console.log(node);
}}
>
{Node}
</Tree>
)}
<VStack h="100%" w="100%">
<Tree
data={treeData}
disableMultiSelection
idAccessor={(d) => d.id}
indent={24}
onActivate={setActivatedNode}
openByDefault={false}
width="100%"
>
{Node}
</Tree>
</VStack>
);
}
66 changes: 0 additions & 66 deletions src/components/_FileTree.tsx

This file was deleted.

29 changes: 29 additions & 0 deletions src/lib/utils/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { join } from "@tauri-apps/api/path";
import naturalCompare from "natural-compare-lite";
import { type FileEntry } from "@/types/bindings";
import { type FileTreeData } from "@/types/file";

export function sortFileEntriesNaturally(
fileEntries: FileEntry[],
): FileEntry[] {
const dirs = fileEntries.filter((e) => e.isDir);
const files = fileEntries.filter((e) => !e.isDir);

const sortedDirs = dirs.sort((a, b) => naturalCompare(a.name, b.name));
const sortedFiles = files.sort((a, b) => naturalCompare(a.name, b.name));

return [...sortedDirs, ...sortedFiles];
}

export async function processFileEntries(
entries: FileEntry[],
): Promise<FileTreeData[]> {
return await Promise.all(
sortFileEntriesNaturally(entries).map(async (f) => ({
...f,
id: await join(f.basePath, f.name),
children:
f.children != null ? await processFileEntries(f.children) : null,
})),
);
}
2 changes: 1 addition & 1 deletion src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default function Page(): ReactElement {
<p.div display="grid" h="100%" placeItems="center">
<VStack>
<p.p fontWeight="bold">Hello!</p.p>
<Link to="/viewer">
<Link to="/license/new">
<p.button
bg={{ base: "blue.200", _hover: "blue.100" }}
cursor="pointer"
Expand Down
38 changes: 38 additions & 0 deletions src/pages/license/new/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { getCurrent } from "@tauri-apps/api/window";
import { VStack, styled as p } from "panda/jsx";
import { useEffect, useState, type ReactElement } from "react";
import { FileTree } from "@/components/FileTree";
import { api } from "@/lib/services/api";
import { type FileEntry } from "@/types/bindings";

export default function Page(): ReactElement {
const [fileEntries, setFileEntries] = useState<FileEntry[]>([]);

useEffect(() => {
const unListen = getCurrent().onDragDropEvent((ev) => {
if (ev.payload.type !== "dropped") {
return;
}
const path = ev.payload.paths.at(0);
if (path == null) throw new Error("Path at 0 is null!");
void api.showFiles(path).then((result) => {
setFileEntries(result);
});
});

return () => {
void unListen.then((fn) => {
fn();
});
};
}, []);

return (
<VStack>
<p.div>
<p.code>{fileEntries.length}</p.code>
</p.div>
<FileTree fileEntries={fileEntries} />
</VStack>
);
}
4 changes: 3 additions & 1 deletion src/pages/viewer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,9 @@ export default function Page(): ReactElement {
if (ev.payload.type !== "dropped") {
return;
}
void api.showFiles(ev.payload.paths[0]).then((result) => {
const path = ev.payload.paths.at(0);
if (path == null) throw new Error("Path at 0 is null!");
void api.showFiles(path).then((result) => {
setFileEntries(result);
});
});
Expand Down
23 changes: 12 additions & 11 deletions src/router.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
// Generouted, changes to this file will be overriden
/* eslint-disable */

import { components, hooks, utils } from "@generouted/react-router/client";
import { components, hooks, utils } from '@generouted/react-router/client'

export type Path = `/` | `/viewer`;
export type Path =
| `/`
| `/license/new`
| `/viewer`

export type Params = {};
export type Params = {

}

export type ModalPath = never;
export type ModalPath = never

export const { Link, Navigate } = components<Path, Params>();
export const { useModals, useNavigate, useParams } = hooks<
Path,
Params,
ModalPath
>();
export const { redirect } = utils<Path, Params>();
export const { Link, Navigate } = components<Path, Params>()
export const { useModals, useNavigate, useParams } = hooks<Path, Params, ModalPath>()
export const { redirect } = utils<Path, Params>()
5 changes: 5 additions & 0 deletions src/types/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { type FileEntry } from "./bindings";

export type FileTreeData = FileEntry & {
id: string;
};
16 changes: 16 additions & 0 deletions src/types/utils.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,18 @@
export type Override<T, U extends { [Key in keyof T]?: unknown }> = Omit<
T,
keyof U
> &
U;

export type Entries<T> = Array<
keyof T extends infer U ? (U extends keyof T ? [U, T[U]] : never) : never
>;

export type ArrayElem<ArrayType extends readonly unknown[]> =
ArrayType extends ReadonlyArray<infer ElementType> ? ElementType : never;

export type OmitStrict<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

export type Nullable<T> = T | null | undefined;

export type State<T> = [T, (value: T) => void];
2 changes: 2 additions & 0 deletions vite.config.mts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineConfig } from "vite";
import { nodePolyfills } from "vite-plugin-node-polyfills";
import { viteStaticCopy } from "vite-plugin-static-copy";
import react from "@vitejs/plugin-react";
import paths from "vite-tsconfig-paths";
Expand All @@ -18,6 +19,7 @@ export default defineConfig(async () => ({
},
],
}),
nodePolyfills(),
],

// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
Expand Down

0 comments on commit 3939a70

Please sign in to comment.