2024-10-10 10:31:30 +02:00
|
|
|
import {
|
2024-10-16 10:12:36 +02:00
|
|
|
Check,
|
2024-10-15 12:20:40 +02:00
|
|
|
Copy,
|
2024-10-10 10:31:30 +02:00
|
|
|
Download,
|
|
|
|
|
FilesIcon,
|
|
|
|
|
Folder,
|
|
|
|
|
Plus,
|
|
|
|
|
ReceiptText,
|
|
|
|
|
X,
|
|
|
|
|
} from "lucide-react";
|
2024-10-16 10:12:36 +02:00
|
|
|
import { ChangeEvent, useEffect, useState } from "react";
|
2024-09-16 12:38:56 +02:00
|
|
|
import { PrettyBytes } from "../../utils/bytes";
|
|
|
|
|
import { Dates } from "../../utils/dates";
|
2024-08-29 18:45:52 +02:00
|
|
|
import "./Files.css";
|
2024-10-16 10:12:36 +02:00
|
|
|
import { ICON_SIZE } from "../../utils/constants";
|
2024-08-29 18:45:52 +02:00
|
|
|
import {
|
|
|
|
|
ButtonIcon,
|
|
|
|
|
WebFileIcon,
|
2024-09-10 18:49:16 +02:00
|
|
|
Tabs,
|
2024-10-10 10:31:30 +02:00
|
|
|
Input,
|
|
|
|
|
Button,
|
|
|
|
|
TabProps,
|
2024-10-16 10:12:36 +02:00
|
|
|
Table,
|
|
|
|
|
Row,
|
|
|
|
|
Cell,
|
2024-08-30 17:26:27 +02:00
|
|
|
} from "@codex-storage/marketplace-ui-components";
|
2024-08-29 18:45:52 +02:00
|
|
|
import { FileDetails } from "./FileDetails.tsx";
|
|
|
|
|
import { useData } from "../../hooks/useData.tsx";
|
2024-10-10 10:31:30 +02:00
|
|
|
import { WebStorage } from "../../utils/web-storage.ts";
|
|
|
|
|
import { CodexSdk } from "../../sdk/codex.ts";
|
|
|
|
|
import { FolderButton } from "./FolderButton.tsx";
|
|
|
|
|
import { classnames } from "../../utils/classnames.ts";
|
2024-10-16 10:12:36 +02:00
|
|
|
import { CodexDataContent } from "@codex-storage/sdk-js";
|
|
|
|
|
import { Files as F } from "../../utils/files.ts";
|
2024-08-29 18:45:52 +02:00
|
|
|
|
2024-10-16 10:12:36 +02:00
|
|
|
type SortFn = (a: CodexDataContent, b: CodexDataContent) => number;
|
2024-08-29 18:45:52 +02:00
|
|
|
|
|
|
|
|
export function Files() {
|
|
|
|
|
const files = useData();
|
2024-09-10 18:49:16 +02:00
|
|
|
const [index, setIndex] = useState(0);
|
2024-10-10 10:31:30 +02:00
|
|
|
const [folder, setFolder] = useState("");
|
|
|
|
|
const [folders, setFolders] = useState<[string, string[]][]>([]);
|
|
|
|
|
const [error, setError] = useState("");
|
2024-10-16 10:12:36 +02:00
|
|
|
const [details, setDetails] = useState<CodexDataContent | null>(null);
|
|
|
|
|
const [sortFn, setSortFn] = useState<SortFn | null>(null);
|
|
|
|
|
const [filters, setFilters] = useState<string[]>([]);
|
2024-08-29 18:45:52 +02:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2024-10-10 10:31:30 +02:00
|
|
|
WebStorage.folders.list().then((items) => setFolders(items));
|
2024-08-29 18:45:52 +02:00
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const onClose = () => {
|
2024-10-16 10:12:36 +02:00
|
|
|
setDetails(null);
|
2024-08-29 18:45:52 +02:00
|
|
|
};
|
|
|
|
|
|
2024-10-10 10:31:30 +02:00
|
|
|
const onTabChange = async (i: number) => setIndex(i);
|
2024-08-29 18:45:52 +02:00
|
|
|
|
2024-10-16 10:12:36 +02:00
|
|
|
const onDetails = (cid: string) => {
|
|
|
|
|
const d = files.find((file) => file.cid === cid);
|
2024-08-29 18:45:52 +02:00
|
|
|
|
2024-10-16 10:12:36 +02:00
|
|
|
if (d) {
|
|
|
|
|
setDetails(d);
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-10-10 10:31:30 +02:00
|
|
|
|
|
|
|
|
const onFolderChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
const val = e.currentTarget.value;
|
|
|
|
|
setFolder(val);
|
|
|
|
|
setError("");
|
|
|
|
|
|
|
|
|
|
if (!val) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (e.currentTarget.checkValidity()) {
|
|
|
|
|
if (folders.length >= 5) {
|
|
|
|
|
setError("5 folders limit reached");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2024-10-15 12:20:40 +02:00
|
|
|
|
|
|
|
|
if (folders.find(([folder]) => folder === val)) {
|
|
|
|
|
setError("This folder already exists");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2024-08-29 18:45:52 +02:00
|
|
|
} else {
|
2024-10-10 10:31:30 +02:00
|
|
|
setError("9 alpha characters maximum");
|
2024-08-29 18:45:52 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-10-10 10:31:30 +02:00
|
|
|
const onFolderCreate = () => {
|
|
|
|
|
WebStorage.folders.create(folder);
|
2024-08-29 18:45:52 +02:00
|
|
|
|
2024-10-10 10:31:30 +02:00
|
|
|
setFolder("");
|
|
|
|
|
setFolders([...folders, [folder, []]]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onFolderDelete = (val: string) => {
|
|
|
|
|
WebStorage.folders.delete(val);
|
|
|
|
|
|
|
|
|
|
const currentIndex = folders.findIndex(([name]) => name === val);
|
|
|
|
|
|
|
|
|
|
if (currentIndex + 1 == index) {
|
|
|
|
|
setIndex(index - 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setFolders(folders.filter(([name]) => name !== val));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onFolderToggle = (cid: string, folder: string) => {
|
|
|
|
|
const current = folders.find(([name]) => name === folder);
|
|
|
|
|
|
|
|
|
|
if (!current) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [, files] = current;
|
|
|
|
|
|
|
|
|
|
if (files.includes(cid)) {
|
|
|
|
|
WebStorage.folders.deleteFile(folder, cid);
|
|
|
|
|
|
|
|
|
|
setFolders(
|
|
|
|
|
folders.map(([name, files]) =>
|
|
|
|
|
name === folder
|
|
|
|
|
? [name, files.filter((id) => id !== cid)]
|
|
|
|
|
: [name, files]
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
WebStorage.folders.addFile(folder, cid);
|
|
|
|
|
|
|
|
|
|
setFolders(
|
|
|
|
|
folders.map(([name, files]) =>
|
|
|
|
|
name === folder ? [name, [...files, cid]] : [name, files]
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const tabs: TabProps[] = folders.map(([folder]) => ({
|
|
|
|
|
label: folder,
|
|
|
|
|
Icon: () => <Folder size={"1rem"}></Folder>,
|
2024-10-15 12:20:40 +02:00
|
|
|
IconAfter:
|
|
|
|
|
folder === "Favorites"
|
|
|
|
|
? undefined
|
|
|
|
|
: () => (
|
|
|
|
|
<X
|
|
|
|
|
size={"1rem"}
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
|
|
|
|
onFolderDelete(folder);
|
|
|
|
|
}}></X>
|
|
|
|
|
),
|
2024-10-10 10:31:30 +02:00
|
|
|
}));
|
|
|
|
|
|
2024-10-16 10:12:36 +02:00
|
|
|
const onSortBySize = (state: "" | "asc" | "desc") => {
|
|
|
|
|
if (!state) {
|
|
|
|
|
setSortFn(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setSortFn(
|
|
|
|
|
() => (a: CodexDataContent, b: CodexDataContent) =>
|
|
|
|
|
state === "desc"
|
|
|
|
|
? b.manifest.datasetSize - a.manifest.datasetSize
|
|
|
|
|
: a.manifest.datasetSize - b.manifest.datasetSize
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onSortByDate = (state: "" | "asc" | "desc") => {
|
|
|
|
|
if (!state) {
|
|
|
|
|
setSortFn(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setSortFn(
|
|
|
|
|
() => (a: CodexDataContent, b: CodexDataContent) =>
|
|
|
|
|
state === "desc"
|
|
|
|
|
? new Date(b.manifest.uploadedAt).getTime() -
|
|
|
|
|
new Date(a.manifest.uploadedAt).getTime()
|
|
|
|
|
: new Date(a.manifest.uploadedAt).getTime() -
|
|
|
|
|
new Date(b.manifest.uploadedAt).getTime()
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onToggleFilter = (filter: string) =>
|
|
|
|
|
filters.includes(filter)
|
|
|
|
|
? setFilters(filters.filter((f) => f !== filter))
|
|
|
|
|
: setFilters([...filters, filter]);
|
2024-10-15 12:20:40 +02:00
|
|
|
|
|
|
|
|
const onCopy = (cid: string) => navigator.clipboard.writeText(cid);
|
2024-08-29 18:45:52 +02:00
|
|
|
|
2024-10-10 10:31:30 +02:00
|
|
|
tabs.unshift({
|
|
|
|
|
label: "All files",
|
|
|
|
|
Icon: () => <FilesIcon size={"1rem"}></FilesIcon>,
|
|
|
|
|
});
|
2024-10-15 12:20:40 +02:00
|
|
|
|
2024-10-10 10:31:30 +02:00
|
|
|
const items =
|
|
|
|
|
index === 0
|
|
|
|
|
? files
|
|
|
|
|
: files.filter((file) => folders[index - 1][1].includes(file.cid));
|
2024-08-29 18:45:52 +02:00
|
|
|
|
2024-10-10 10:31:30 +02:00
|
|
|
const url = CodexSdk.url() + "/api/codex/v1/data/";
|
2024-08-29 18:45:52 +02:00
|
|
|
|
2024-10-16 10:12:36 +02:00
|
|
|
const headers = [
|
|
|
|
|
["file"],
|
|
|
|
|
["size", onSortBySize],
|
|
|
|
|
["date", onSortByDate],
|
|
|
|
|
["actions"],
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const types = Array.from(
|
|
|
|
|
new Set(files.map((file) => F.type(file.manifest.mimetype)))
|
|
|
|
|
);
|
|
|
|
|
const filtered = items.filter(
|
|
|
|
|
(item) =>
|
|
|
|
|
filters.length === 0 || filters.includes(F.type(item.manifest.mimetype))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const sorted = sortFn ? [...filtered].sort(sortFn) : filtered;
|
|
|
|
|
const rows =
|
|
|
|
|
sorted.map((c) => (
|
|
|
|
|
<Row
|
|
|
|
|
cells={[
|
|
|
|
|
<Cell>
|
|
|
|
|
<div className="files-cell-file">
|
|
|
|
|
<WebFileIcon type={c.manifest.mimetype} />
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<b>{c.manifest.filename}</b>
|
|
|
|
|
<div>
|
|
|
|
|
<small className="files-fileMeta">{c.cid}</small>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Cell>,
|
|
|
|
|
<Cell>{PrettyBytes(c.manifest.datasetSize)}</Cell>,
|
|
|
|
|
<Cell>{Dates.format(c.manifest.uploadedAt).toString()}</Cell>,
|
|
|
|
|
<Cell>
|
|
|
|
|
<div className="files-fileActions">
|
|
|
|
|
<ButtonIcon
|
|
|
|
|
variant="small"
|
|
|
|
|
animation="bounce"
|
|
|
|
|
onClick={() => window.open(url + c.cid, "_blank")}
|
|
|
|
|
Icon={(props) => (
|
|
|
|
|
<Download size={ICON_SIZE} {...props} />
|
|
|
|
|
)}></ButtonIcon>
|
|
|
|
|
|
|
|
|
|
<FolderButton
|
|
|
|
|
folders={folders.map(([folder, files]) => [
|
|
|
|
|
folder,
|
|
|
|
|
files.includes(c.cid),
|
|
|
|
|
])}
|
|
|
|
|
onFolderToggle={(folder) => onFolderToggle(c.cid, folder)}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<ButtonIcon
|
|
|
|
|
variant="small"
|
|
|
|
|
onClick={() => onCopy(c.cid)}
|
|
|
|
|
animation="buzz"
|
|
|
|
|
Icon={(props) => (
|
|
|
|
|
<Copy size={ICON_SIZE} {...props} />
|
|
|
|
|
)}></ButtonIcon>
|
|
|
|
|
|
|
|
|
|
<ButtonIcon
|
|
|
|
|
variant="small"
|
|
|
|
|
onClick={() => onDetails(c.cid)}
|
|
|
|
|
Icon={() => <ReceiptText size={ICON_SIZE} />}></ButtonIcon>
|
|
|
|
|
</div>
|
|
|
|
|
</Cell>,
|
|
|
|
|
]}></Row>
|
|
|
|
|
)) || [];
|
|
|
|
|
|
2024-08-29 18:45:52 +02:00
|
|
|
return (
|
|
|
|
|
<div className="files">
|
|
|
|
|
<div className="files-header">
|
2024-10-10 10:31:30 +02:00
|
|
|
<div className="files-headerLeft">
|
|
|
|
|
<div className="files-title">Files</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="files-headerRight">
|
|
|
|
|
<div>
|
|
|
|
|
<Input
|
|
|
|
|
id="folder"
|
|
|
|
|
inputClassName={classnames(["files-folders"])}
|
|
|
|
|
isInvalid={folder !== "" && !!error}
|
|
|
|
|
value={folder}
|
|
|
|
|
required={true}
|
2024-10-15 12:20:40 +02:00
|
|
|
pattern="[A-Za-z0-9_\-]*"
|
2024-10-10 10:31:30 +02:00
|
|
|
maxLength={9}
|
|
|
|
|
helper={error || "Enter the folder name"}
|
|
|
|
|
onChange={onFolderChange}></Input>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
label="Folder"
|
|
|
|
|
Icon={Plus}
|
|
|
|
|
disabled={!!error || !folder}
|
|
|
|
|
onClick={onFolderCreate}></Button>
|
|
|
|
|
</div>
|
2024-08-29 18:45:52 +02:00
|
|
|
</div>
|
|
|
|
|
|
2024-10-10 10:31:30 +02:00
|
|
|
<Tabs onTabChange={onTabChange} tabIndex={index} tabs={tabs}></Tabs>
|
|
|
|
|
|
2024-10-16 10:12:36 +02:00
|
|
|
<div className="files-filters">
|
|
|
|
|
{types.map((type) => (
|
|
|
|
|
<span
|
|
|
|
|
key={type}
|
|
|
|
|
className={classnames(
|
|
|
|
|
["files-filter"],
|
|
|
|
|
["files-filter--active", filters.includes(type)]
|
|
|
|
|
)}
|
|
|
|
|
onClick={() => onToggleFilter(type)}>
|
|
|
|
|
<span>{type}</span> <Check size={"1rem"}></Check>
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
2024-08-29 18:45:52 +02:00
|
|
|
<div className="files-fileBody">
|
2024-10-16 10:12:36 +02:00
|
|
|
<Table headers={headers as any} rows={rows} />
|
2024-08-29 18:45:52 +02:00
|
|
|
</div>
|
|
|
|
|
|
2024-10-16 10:12:36 +02:00
|
|
|
<FileDetails onClose={onClose} details={details} />
|
2024-08-29 18:45:52 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|