diff --git a/src/components/files/DirectoryTable.tsx b/src/components/files/DirectoryTable.tsx index 312cb4b7..0891b3ff 100644 --- a/src/components/files/DirectoryTable.tsx +++ b/src/components/files/DirectoryTable.tsx @@ -22,38 +22,64 @@ import { DataTable, type DataTableSortStatus } from "mantine-datatable"; import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import * as classes from "./DirectoryTable.css"; -import type { FileMetadata } from "./file"; +import type { Directory, FileMetadata } from "./file"; import { getStats } from "./opening"; -// function recursiveSort( -// files: FileMetadata[], -// sort: DataTableSortStatus, -// ): MetadataOrEntry[] { -// return files -// .map((f) => { -// if (!f.children) return f; -// return { -// ...f, -// children: recursiveSort(f.children, sort), -// }; -// }) -// .sort((a, b) => { -// if (sort.direction === "desc") { -// if (sort.columnAccessor === "name") { -// return b.name.localeCompare(a.name); -// } -// // @ts-ignore -// return b[sort.columnAccessor] > a[sort.columnAccessor] ? 1 : -1; -// } -// if (sort.columnAccessor === "name") { -// return a.name.localeCompare(b.name); -// } -// // @ts-ignore -// return a[sort.columnAccessor] > b[sort.columnAccessor] ? 1 : -1; -// }); -// } +function flattenFiles( + files: (FileMetadata | Directory)[], +): (FileMetadata | Directory)[] { + return files.flatMap((f) => + f.type === "directory" ? flattenFiles(f.children) : [f], + ); +} + +function recursiveSort( + files: (FileMetadata | Directory)[], + sort: DataTableSortStatus, +): (FileMetadata | Directory)[] { + return files + .map((f) => { + if (f.type === "file") return f; + return { + ...f, + children: recursiveSort(f.children, sort), + }; + }) + .sort((a, b) => { + return b.name.localeCompare(a.name, "en", { sensitivity: "base" }); + }) + .filter((f) => { + return f.type === "file" || f.children.length > 0; + }) + .sort((a, b) => { + if (sort.direction === "desc") { + if (sort.columnAccessor === "name") { + return b.name.localeCompare(a.name); + } + // @ts-ignore + return b[sort.columnAccessor] > a[sort.columnAccessor] ? 1 : -1; + } + if (sort.columnAccessor === "name") { + return a.name.localeCompare(b.name); + } + // @ts-ignore + return a[sort.columnAccessor] > b[sort.columnAccessor] ? 1 : -1; + }) + .sort((a, b) => { + if (a.type === "directory" && b.type === "file") { + return -1; + } + if (a.type === "directory" && b.type === "directory") { + return 0; + } + if (a.type === "file" && b.type === "file") { + return 0; + } + return 1; + }); +} -type SortStatus = DataTableSortStatus; +type SortStatus = DataTableSortStatus; const sortStatusStorageId = `${DirectoryTable.name}-sort-status` as const; const sortStatusAtom = atomWithStorage( sortStatusStorageId, @@ -74,9 +100,9 @@ export default function DirectoryTable({ search, filter, }: { - files: FileMetadata[] | undefined; + files: (FileMetadata | Directory)[] | undefined; isLoading: boolean; - setFiles: (files: FileMetadata[]) => void; + setFiles: (files: (FileMetadata | Directory)[]) => void; selectedFile: FileMetadata | null; setSelectedFile: (file: FileMetadata) => void; search: string; @@ -84,8 +110,7 @@ export default function DirectoryTable({ }) { const [sort, setSort] = useAtom(sortStatusAtom); - // const flattedFiles = useMemo(() => flattenFiles(files ?? []), [files]); - const flattedFiles = files ?? []; + const flattedFiles = useMemo(() => flattenFiles(files ?? []), [files]); const fuse = useMemo( () => new Fuse(flattedFiles ?? [], { @@ -94,42 +119,42 @@ export default function DirectoryTable({ [flattedFiles], ); - const filteredFiles = files ?? []; + let filteredFiles = files ?? []; - // if (search) { - // const searchResults = fuse.search(search); - // filteredFiles = filteredFiles - // .filter((f) => searchResults.some((r) => r.item.path.includes(f.path))) - // .map((f) => { - // if (!f.children) return f; - // const children = f.children.filter((c) => - // searchResults.some((r) => r.item.path.includes(c.path)), - // ); - // return { - // ...f, - // children, - // }; - // }); - // } - // if (filter) { - // const typeFilteredFiles = flattedFiles.filter( - // (f) => f.metadata?.type === filter, - // ); - // filteredFiles = filteredFiles - // .filter((f) => typeFilteredFiles.some((r) => r.path.includes(f.path))) - // .map((f) => { - // if (!f.children) return f; - // const children = f.children.filter((c) => - // typeFilteredFiles.some((r) => r.path.includes(c.path)), - // ); - // return { - // ...f, - // children, - // }; - // }); - // } + if (search) { + const searchResults = fuse.search(search); + filteredFiles = filteredFiles + .filter((f) => searchResults.some((r) => r.item.path.includes(f.path))) + .map((f) => { + if (f.type === "file") return f; + const children = f.children.filter((c) => + searchResults.some((r) => r.item.path.includes(c.path)), + ); + return { + ...f, + children, + }; + }); + } + if (filter) { + const typeFilteredFiles = flattedFiles.filter( + (f) => (f.type === "file" && f.metadata.type) === filter, + ); + filteredFiles = filteredFiles + .filter((f) => typeFilteredFiles.some((r) => r.path.includes(f.path))) + .map((f) => { + if (f.type === "file") return f; + const children = f.children.filter((c) => + typeFilteredFiles.some((r) => r.path.includes(c.path)), + ); + return { + ...f, + children, + }; + }); + } - // filteredFiles = recursiveSort(filteredFiles, sort); + filteredFiles = recursiveSort(filteredFiles, sort); return ( void; + setFiles: (files: (FileMetadata | Directory)[]) => void; selected: FileMetadata | null; setSelectedFile: (file: FileMetadata) => void; - sort: DataTableSortStatus; + sort: DataTableSortStatus; setSort: (sort: SortStatus) => void; }) { const { t } = useTranslation(); const [expandedIds, setExpandedIds] = useState([]); - // const expandedFiles = expandedIds.filter((id) => - // files?.find((f) => f.path === id && f.children), - // ); + const expandedFiles = expandedIds.filter((id) => + files?.find((f) => f.path === id && f.type === "directory"), + ); const navigate = useNavigate(); const [, setTabs] = useAtom(tabsAtom); const setActiveTab = useSetAtom(activeTabAtom); @@ -211,10 +236,10 @@ function Table({ record.path === selected?.path ? classes.selected : "" } sortStatus={sort} - // onRowDoubleClick={({ record }) => { - // if (record.children) return; - // openFile(record as FileMetadata); - // }} + onRowDoubleClick={({ record }) => { + if (record.type === "directory") return; + openFile(record); + }} onSortStatusChange={setSort} columns={[ { @@ -224,7 +249,7 @@ function Table({ render: (row) => ( - {row.children && ( + {row.type === "directory" && ( )} {row.name} - {row.metadata?.type === "repertoire" && ( + {row.type === "file" && row.metadata.type === "repertoire" && ( )} @@ -246,7 +271,9 @@ function Table({ title: "Type", width: 100, render: (row) => - t(`Files.FileType.${capitalize(row.metadata?.type || "Folder")}`), + t( + `Files.FileType.${capitalize((row.type === "file" && row.metadata.type) || "Folder")}`, + ), }, { accessor: "lastModified", @@ -254,7 +281,7 @@ function Table({ textAlign: "right", width: 200, render: (row) => { - if (!row.lastModified) return null; + if (row.type === "directory") return null; return ( {dayjs(row.lastModified * 1000).format("DD MMM YYYY HH:mm")} @@ -264,58 +291,58 @@ function Table({ }, ]} records={files} - // rowExpansion={{ - // allowMultiple: true, - // expanded: { - // recordIds: expandedFiles, - // onRecordIdsChange: setExpandedIds, - // }, - // content: ({ record }) => - // record.children && ( - //
- // ), - // }} + rowExpansion={{ + allowMultiple: true, + expanded: { + recordIds: expandedFiles, + onRecordIdsChange: setExpandedIds, + }, + content: ({ record }) => + record.type === "directory" && ( +
+ ), + }} onRowClick={({ record }) => { - if (!record.children) { - setSelectedFile(record as FileMetadata); + if (record.type === "file") { + setSelectedFile(record); } }} - // onRowContextMenu={({ record, event }) => { - // return showContextMenu([ - // { - // key: "open-file", - // icon: , - // disabled: !!record.children, - // onClick: () => { - // if (record.children) return; - // openFile(record as FileMetadata); - // }, - // }, - // { - // key: "delete-file", - // icon: , - // title: "Delete", - // color: "red", - // onClick: async () => { - // if (record.children) { - // await remove(record.path, { recursive: true }); - // } else { - // await remove(record.path); - // } - // setFiles(files?.filter((f) => record.path.includes(f.path))); - // }, - // }, - // ])(event); - // }} + onRowContextMenu={({ record, event }) => { + return showContextMenu([ + { + key: "open-file", + icon: , + disabled: record.type === "directory", + onClick: () => { + if (record.type === "directory") return; + openFile(record); + }, + }, + { + key: "delete-file", + icon: , + title: "Delete", + color: "red", + onClick: async () => { + if (record.type === "directory") { + await remove(record.path, { recursive: true }); + } else { + await remove(record.path); + } + setFiles(files?.filter((f) => record.path.includes(f.path))); + }, + }, + ])(event); + }} /> ); } diff --git a/src/components/files/FilesPage.tsx b/src/components/files/FilesPage.tsx index 326b143d..97742a3e 100644 --- a/src/components/files/FilesPage.tsx +++ b/src/components/files/FilesPage.tsx @@ -45,34 +45,10 @@ const useFileDirectory = (dir: string) => { const { data, error, isLoading, mutate } = useSWR( "file-directory", async () => { - // const files = await readDir(dir, { recursive: true }); - // const filesInfo = await processFiles( - // files.filter((f) => !f.name?.startsWith(".")) as MetadataOrEntry[], - // ); const entries = await readDir(dir); const allEntries = processEntriesRecursively(dir, entries); - console.log({ allEntries }); return allEntries; - // return allEntries - // .sort((a, b) => { - // return b.name.localeCompare(a.name, "en", { sensitivity: "base" }); - // }) - // .filter((f) => { - // return f.children === undefined || f.children?.length > 0; - // }) - // .sort((a, b) => { - // if (a.children != null && b.children == null) { - // return 1; - // } - // if (a.children != null && b.children != null) { - // return 0; - // } - // if (a.children == null && b.children == null) { - // return 0; - // } - // return -1; - // }); }, ); console.log(error); diff --git a/src/components/files/Modals.tsx b/src/components/files/Modals.tsx index b33b8b68..1d7d0f64 100644 --- a/src/components/files/Modals.tsx +++ b/src/components/files/Modals.tsx @@ -13,7 +13,7 @@ import { rename, writeTextFile } from "@tauri-apps/plugin-fs"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import GenericCard from "../common/GenericCard"; -import type { FileMetadata, FileType } from "./file"; +import type { Directory, FileMetadata, FileType } from "./file"; const FILE_TYPES = [ { label: "Game", value: "game" }, @@ -32,8 +32,8 @@ export function CreateModal({ }: { opened: boolean; setOpened: (opened: boolean) => void; - files: FileMetadata[]; - setFiles: (files: FileMetadata[]) => void; + files: (FileMetadata | Directory)[]; + setFiles: (files: (FileMetadata | Directory)[]) => void; setSelected: React.Dispatch>; }) { const { t } = useTranslation(); diff --git a/src/components/files/file.ts b/src/components/files/file.ts index 381128fb..d9b517db 100644 --- a/src/components/files/file.ts +++ b/src/components/files/file.ts @@ -28,6 +28,7 @@ const fileInfoMetadataSchema = z.object({ export type FileInfoMetadata = z.infer; export const fileMetadataSchema = z.object({ + type: z.literal("file"), name: z.string(), path: z.string(), numGames: z.number(), @@ -60,6 +61,7 @@ async function readFileMetadata(path: string): Promise { const fileMetadata = unwrap(await commands.getFileMetadata(path)); const numGames = unwrap(await commands.countPgnGames(path)); return { + type: "file", path, name: (await basename(path)).replace(".pgn", ""), numGames, @@ -68,15 +70,21 @@ async function readFileMetadata(path: string): Promise { }; } +export type Directory = { + type: "directory"; + children: (FileMetadata | Directory)[]; + path: string; + name: string; +}; + export async function processEntriesRecursively( parent: string, entries: DirEntry[], ) { - const allEntries: FileMetadata[] = []; + const allEntries: (FileMetadata | Directory)[] = []; for (const entry of entries) { if (entry.isFile) { const metadata = await readFileMetadata(await join(parent, entry.name)); - console.log(metadata); if (!metadata) continue; allEntries.push(metadata); } @@ -86,7 +94,12 @@ export async function processEntriesRecursively( dir, await readDir(dir, { baseDir: BaseDirectory.AppLocalData }), ); - allEntries.push(...newEntries); + allEntries.push({ + type: "directory", + name: entry.name, + path: dir, + children: newEntries, + }); } } return allEntries; diff --git a/src/components/tabs/ImportModal.tsx b/src/components/tabs/ImportModal.tsx index b90e9ccc..f0e8b6f4 100644 --- a/src/components/tabs/ImportModal.tsx +++ b/src/components/tabs/ImportModal.tsx @@ -85,6 +85,7 @@ export default function ImportModal({ fileInfo = newFile.value; } else { fileInfo = { + type: "file", path: file, numGames: count, name: filename, diff --git a/src/utils/files.ts b/src/utils/files.ts index 7e85453d..f5b5abfd 100644 --- a/src/utils/files.ts +++ b/src/utils/files.ts @@ -27,6 +27,7 @@ export async function openFile( const input = unwrap(await commands.readGames(file, 0, 0))[0]; const fileInfo = { + type: "file" as const, metadata: { tags: [], type: "game" as const, @@ -71,6 +72,7 @@ export async function createFile({ await writeTextFile(file, pgn || makePgn(defaultGame())); await writeTextFile(file.replace(".pgn", ".info"), JSON.stringify(metadata)); return Result.ok({ + type: "file", name: filename, path: file, numGames: 1, diff --git a/src/utils/tabs.ts b/src/utils/tabs.ts index eb6a9bc3..18891c53 100644 --- a/src/utils/tabs.ts +++ b/src/utils/tabs.ts @@ -114,6 +114,7 @@ export async function saveToFile({ return { ...prev, file: { + type: "file", name: userChoice, path: userChoice, numGames: 1,