From b78c073066fdb4b12194d5c5ecc8665eeac03a9a Mon Sep 17 00:00:00 2001 From: Kieran Date: Thu, 26 Sep 2024 10:46:39 +0100 Subject: [PATCH] feat: list files grid/table --- ui_src/src/App.tsx | 2 +- ui_src/src/const.ts | 45 ++++++++ ui_src/src/upload/nip96.ts | 13 ++- ui_src/src/views/files.tsx | 182 ++++++++++++++++++++++++++--- ui_src/src/views/upload.tsx | 225 +++++++++++++++++++----------------- ui_src/tsconfig.app.json | 2 +- ui_src/vite.config.ts | 2 +- 7 files changed, 345 insertions(+), 126 deletions(-) create mode 100644 ui_src/src/const.ts diff --git a/ui_src/src/App.tsx b/ui_src/src/App.tsx index 52a086e..733d289 100644 --- a/ui_src/src/App.tsx +++ b/ui_src/src/App.tsx @@ -3,7 +3,7 @@ import Upload from "./views/upload"; function App() { return ( -
+
diff --git a/ui_src/src/const.ts b/ui_src/src/const.ts new file mode 100644 index 0000000..72823ae --- /dev/null +++ b/ui_src/src/const.ts @@ -0,0 +1,45 @@ +/** + * @constant {number} - Size of 1 kiB + */ +export const kiB = Math.pow(1024, 1); +/** + * @constant {number} - Size of 1 MiB + */ +export const MiB = Math.pow(1024, 2); +/** + * @constant {number} - Size of 1 GiB + */ +export const GiB = Math.pow(1024, 3); +/** + * @constant {number} - Size of 1 TiB + */ +export const TiB = Math.pow(1024, 4); +/** + * @constant {number} - Size of 1 PiB + */ +export const PiB = Math.pow(1024, 5); +/** + * @constant {number} - Size of 1 EiB + */ +export const EiB = Math.pow(1024, 6); +/** + * @constant {number} - Size of 1 ZiB + */ +export const ZiB = Math.pow(1024, 7); +/** + * @constant {number} - Size of 1 YiB + */ +export const YiB = Math.pow(1024, 8); + +export function FormatBytes(b: number, f?: number) { + f ??= 2; + if (b >= YiB) return (b / YiB).toFixed(f) + " YiB"; + if (b >= ZiB) return (b / ZiB).toFixed(f) + " ZiB"; + if (b >= EiB) return (b / EiB).toFixed(f) + " EiB"; + if (b >= PiB) return (b / PiB).toFixed(f) + " PiB"; + if (b >= TiB) return (b / TiB).toFixed(f) + " TiB"; + if (b >= GiB) return (b / GiB).toFixed(f) + " GiB"; + if (b >= MiB) return (b / MiB).toFixed(f) + " MiB"; + if (b >= kiB) return (b / kiB).toFixed(f) + " KiB"; + return b.toFixed(f) + " B"; +} diff --git a/ui_src/src/upload/nip96.ts b/ui_src/src/upload/nip96.ts index 57866ee..abf54fc 100644 --- a/ui_src/src/upload/nip96.ts +++ b/ui_src/src/upload/nip96.ts @@ -22,7 +22,7 @@ export class Nip96 { return this.#info; } - async listFiles(page = 0, count = 50) { + async listFiles(page = 0, count = 10) { const rsp = await this.#req(`?page=${page}&count=${count}`, "GET"); const data = await this.#handleResponse(rsp); return data; @@ -37,7 +37,7 @@ export class Nip96 { const rsp = await this.#req("", "POST", fd); const data = await this.#handleResponse(rsp); - if(data.status !== "success") { + if (data.status !== "success") { throw new Error(data.message); } return data; @@ -93,16 +93,19 @@ export interface Nip96Info { download_url?: string; } -export interface Nip96Status {status: string, message?: string} +export interface Nip96Status { + status: string; + message?: string; +} export type Nip96Result = Nip96Status & { processing_url?: string; nip94_event: NostrEvent; -} +}; export type Nip96FileList = Nip96Status & { count: number; total: number; page: number; files: Array; -} +}; diff --git a/ui_src/src/views/files.tsx b/ui_src/src/views/files.tsx index 8bf3d45..4c00841 100644 --- a/ui_src/src/views/files.tsx +++ b/ui_src/src/views/files.tsx @@ -1,28 +1,180 @@ -export default function FileList({ files }: { files: Array }) { +import { NostrEvent } from "@snort/system"; +import { useState } from "react"; +import { FormatBytes } from "../const"; + +interface FileInfo { + id: string; + url: string; + name?: string; + type?: string; + size?: number; +} + +export default function FileList({ + files, + pages, + page, + onPage, +}: { + files: Array; + pages?: number; + page?: number; + onPage?: (n: number) => void; +}) { + const [viewType, setViewType] = useState<"grid" | "list">("grid"); if (files.length === 0) { return No Files; } - function renderInner(f: File) { - if (f.type.startsWith("image/")) { + function renderInner(f: FileInfo) { + if (f.type?.startsWith("image/")) { return ( - + + ); + } else if (f.type?.startsWith("video/")) { + return ( +
+ Video +
); } } - return ( -
- {files.map((a) => ( + + function getInfo(f: File | NostrEvent): FileInfo { + if ("created_at" in f) { + return { + id: f.tags.find((a) => a[0] === "x")![1], + url: f.tags.find((a) => a[0] === "url")?.at(1), + name: f.content, + type: f.tags.find((a) => a[0] === "m")?.at(1), + size: Number(f.tags.find((a) => a[0] === "size")?.at(1)), + }; + } else { + return { + id: f.name, + url: URL.createObjectURL(f), + name: f.name, + type: f.type, + size: f.size, + }; + } + } + + function pageButtons(page: number, n: number) { + const ret = []; + const start = 0; + + for (let x = start; x < n; x++) { + ret.push(
onPage?.(x)} + className={`bg-neutral-800 hover:bg-neutral-700 min-w-8 text-center cursor-pointer font-bold ${x === start ? "rounded-l-md" : ""} ${x === n - 1 ? "rounded-r-md" : ""} ${page === x ? "bg-neutral-500" : ""}`} > -
- {a.name} -
- {renderInner(a)} + {x + 1} +
, + ); + } + + return ret; + } + + function showGrid() { + return ( +
+ {files.map((a) => { + const info = getInfo(a); + + return ( +
+
+
+ {(info.name?.length ?? 0) === 0 ? "Untitled" : info.name} +
+
+ {info.size && !isNaN(info.size) + ? FormatBytes(info.size, 2) + : ""} +
+ + Link + +
+ {renderInner(info)} +
+ ); + })} +
+ ); + } + + function showList() { + return ( + + + + + + + + + + {files.map((a) => { + const info = getInfo(a); + return ( + + + + + + ); + })} + +
+ Name + + Size + + Actions +
+ {(info.name?.length ?? 0) === 0 ? "" : info.name} + + {info.size && !isNaN(info.size) + ? FormatBytes(info.size, 2) + : ""} + + + Link + +
+ ); + } + + return ( + <> +
+
setViewType("grid")} + className={`bg-neutral-800 hover:bg-neutral-600 min-w-20 text-center cursor-pointer font-bold rounded-l-md ${viewType === "grid" ? "bg-neutral-500" : ""}`} + > + Grid
- ))} -
+
setViewType("list")} + className={`bg-neutral-800 hover:bg-neutral-600 min-w-20 text-center cursor-pointer font-bold rounded-r-md ${viewType === "list" ? "bg-neutral-500" : ""}`} + > + List +
+
+ {viewType === "grid" ? showGrid() : showList()} + {pages !== undefined && ( + <> + Page: +
{pageButtons(page ?? 0, pages)}
+ + )} + ); } diff --git a/ui_src/src/views/upload.tsx b/ui_src/src/views/upload.tsx index f4c622e..fa9cebb 100644 --- a/ui_src/src/views/upload.tsx +++ b/ui_src/src/views/upload.tsx @@ -1,119 +1,138 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import Button from "../components/button"; import FileList from "./files"; import { openFile } from "../upload"; import { Blossom } from "../upload/blossom"; import useLogin from "../hooks/login"; import usePublisher from "../hooks/publisher"; -import { Nip96 } from "../upload/nip96"; +import { Nip96, Nip96FileList } from "../upload/nip96"; export default function Upload() { - const [type, setType] = useState<"blossom" | "nip96">("nip96"); - const [noCompress, setNoCompress] = useState(false); - const [toUpload, setToUpload] = useState(); - const [error, setError] = useState(); - const [results, setResults] = useState>([]); - const login = useLogin(); - const pub = usePublisher(); + const [type, setType] = useState<"blossom" | "nip96">("nip96"); + const [noCompress, setNoCompress] = useState(false); + const [toUpload, setToUpload] = useState(); + const [error, setError] = useState(); + const [results, setResults] = useState>([]); + const [listedFiles, setListedFiles] = useState(); + const [listedPage, setListedPage] = useState(0); - async function doUpload() { - if (!pub) return; - if (!toUpload) return; - try { - setError(undefined); - const url = `${location.protocol}//${location.host}`; - if (type === "blossom") { - const uploader = new Blossom(url, pub); - const result = await uploader.upload(toUpload); - setResults(s => [...s, result]); - } - if (type === "nip96") { - const uploader = new Nip96(url, pub); - await uploader.loadInfo(); - const result = await uploader.upload(toUpload); - setResults(s => [...s, result]); - } - } catch (e) { - if (e instanceof Error) { - setError(e.message.length > 0 ? e.message : "Upload failed"); - } else if (typeof e === "string") { - setError(e); - } else { - setError("Upload failed"); - } - } + const login = useLogin(); + const pub = usePublisher(); + + const url = `${location.protocol}//${location.host}`; + //const url = "https://files.v0l.io"; + async function doUpload() { + if (!pub) return; + if (!toUpload) return; + try { + setError(undefined); + if (type === "blossom") { + const uploader = new Blossom(url, pub); + const result = await uploader.upload(toUpload); + setResults((s) => [...s, result]); + } + if (type === "nip96") { + const uploader = new Nip96(url, pub); + await uploader.loadInfo(); + const result = await uploader.upload(toUpload); + setResults((s) => [...s, result]); + } + } catch (e) { + if (e instanceof Error) { + setError(e.message.length > 0 ? e.message : "Upload failed"); + } else if (typeof e === "string") { + setError(e); + } else { + setError("Upload failed"); + } } + } - async function listUploads() { - if (!pub) return; - try { - setError(undefined); - const url = `${location.protocol}//${location.host}`; - const uploader = new Nip96(url, pub); - await uploader.loadInfo(); - const result = await uploader.listFiles(); - setResults(s => [...s, result]); - } catch (e) { - if (e instanceof Error) { - setError(e.message.length > 0 ? e.message : "Upload failed"); - } else if (typeof e === "string") { - setError(e); - } else { - setError("List files failed"); - } - } + async function listUploads(n: number) { + if (!pub) return; + try { + setError(undefined); + const uploader = new Nip96(url, pub); + await uploader.loadInfo(); + const result = await uploader.listFiles(n, 12); + setListedFiles(result); + } catch (e) { + if (e instanceof Error) { + setError(e.message.length > 0 ? e.message : "Upload failed"); + } else if (typeof e === "string") { + setError(e); + } else { + setError("List files failed"); + } } + } - return ( -
-

- Welcome to {window.location.hostname} -

-
- Upload Method -
-
-
setType("blossom")} - > - Blossom - -
-
setType("nip96")} - > - NIP-96 - -
-
+ useEffect(() => { + listUploads(listedPage); + }, [listedPage]); - {type === "nip96" && ( -
setNoCompress((s) => !s)} - > - Disable Compression - -
- )} - - - - - - {error && {error}} -
{JSON.stringify(results, undefined, 2)}
+ return ( +
+

+ Welcome to {window.location.hostname} +

+
+ Upload Method +
+
+
setType("blossom")} + > + Blossom +
- ); +
setType("nip96")} + > + NIP-96 + +
+
+ + {type === "nip96" && ( +
setNoCompress((s) => !s)} + > + Disable Compression + +
+ )} + + + + + + {listedFiles && ( + setListedPage(x)} + /> + )} + {error && {error}} +
+        {JSON.stringify(results, undefined, 2)}
+      
+
+ ); } diff --git a/ui_src/tsconfig.app.json b/ui_src/tsconfig.app.json index 9ef45e0..23807a5 100644 --- a/ui_src/tsconfig.app.json +++ b/ui_src/tsconfig.app.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES2020", + "target": "ESNext", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/ui_src/vite.config.ts b/ui_src/vite.config.ts index d0742db..97090b8 100644 --- a/ui_src/vite.config.ts +++ b/ui_src/vite.config.ts @@ -1,6 +1,6 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; -import { viteSingleFile } from "vite-plugin-singlefile" +import { viteSingleFile } from "vite-plugin-singlefile"; // https://vitejs.dev/config/ export default defineConfig({