mirror of
https://git.v0l.io/Kieran/dtan.git
synced 2025-01-18 04:41:32 +00:00
feat: stupid search
This commit is contained in:
parent
1d16d61ea1
commit
acd4c8ec3f
@ -1,6 +1,16 @@
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Categories } from "../const";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function Search() {
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [term, setTerm] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setTerm(params.term ?? "");
|
||||
}, [params.term]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-3 flex-wrap">
|
||||
@ -11,7 +21,18 @@ export function Search() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<input type="text" placeholder="Search.." className="p-3 rounded grow" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search.."
|
||||
className="p-3 rounded grow"
|
||||
value={term}
|
||||
onChange={(e) => setTerm(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key == "Enter") {
|
||||
navigate(`/search/${encodeURIComponent(term)}`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ export function TorrentList({ items }: { items: Array<TaggedNostrEvent> }) {
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((a) => (
|
||||
<TorrentTableEntry item={a} />
|
||||
<TorrentTableEntry item={a} key={a.id} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -42,9 +42,11 @@ export function useLogin() {
|
||||
() => LoginState.snapshot(),
|
||||
);
|
||||
const system = useContext(SnortContext);
|
||||
return session ? {
|
||||
...session,
|
||||
builder: new EventPublisher(new Nip7Signer(), session.publicKey),
|
||||
system
|
||||
} : undefined;
|
||||
return session
|
||||
? {
|
||||
...session,
|
||||
builder: new EventPublisher(new Nip7Signer(), session.publicKey),
|
||||
system,
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
@ -10,10 +10,11 @@ import { ProfilePage } from "./page/profile";
|
||||
import { NewPage } from "./page/new";
|
||||
import { TorrentPage } from "./page/torrent";
|
||||
import { SnortSystemDb } from "@snort/system-web";
|
||||
import { SearchPage } from "./page/search";
|
||||
|
||||
const db = new SnortSystemDb();
|
||||
const System = new NostrSystem({
|
||||
db
|
||||
db,
|
||||
});
|
||||
const Routes = [
|
||||
{
|
||||
@ -42,6 +43,10 @@ const Routes = [
|
||||
path: "/e/:id",
|
||||
element: <TorrentPage />,
|
||||
},
|
||||
{
|
||||
path: "/search/:term?",
|
||||
element: <SearchPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
] as Array<RouteObject>;
|
||||
|
438
src/page/new.tsx
438
src/page/new.tsx
@ -9,234 +9,234 @@ import { bytesToHex } from "@noble/hashes/utils";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
async function openFile(): Promise<File | undefined> {
|
||||
return new Promise((resolve) => {
|
||||
const elm = document.createElement("input");
|
||||
let lock = false;
|
||||
elm.type = "file";
|
||||
elm.accept = ".torrent";
|
||||
const handleInput = (e: Event) => {
|
||||
lock = true;
|
||||
const elm = e.target as HTMLInputElement;
|
||||
if ((elm.files?.length ?? 0) > 0) {
|
||||
resolve(elm.files![0]);
|
||||
} else {
|
||||
resolve(undefined);
|
||||
}
|
||||
};
|
||||
return new Promise((resolve) => {
|
||||
const elm = document.createElement("input");
|
||||
let lock = false;
|
||||
elm.type = "file";
|
||||
elm.accept = ".torrent";
|
||||
const handleInput = (e: Event) => {
|
||||
lock = true;
|
||||
const elm = e.target as HTMLInputElement;
|
||||
if ((elm.files?.length ?? 0) > 0) {
|
||||
resolve(elm.files![0]);
|
||||
} else {
|
||||
resolve(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
elm.onchange = (e) => handleInput(e);
|
||||
elm.click();
|
||||
window.addEventListener(
|
||||
"focus",
|
||||
() => {
|
||||
setTimeout(() => {
|
||||
if (!lock) {
|
||||
console.debug("FOCUS WINDOW UPLOAD");
|
||||
resolve(undefined);
|
||||
}
|
||||
}, 300);
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
elm.onchange = (e) => handleInput(e);
|
||||
elm.click();
|
||||
window.addEventListener(
|
||||
"focus",
|
||||
() => {
|
||||
setTimeout(() => {
|
||||
if (!lock) {
|
||||
console.debug("FOCUS WINDOW UPLOAD");
|
||||
resolve(undefined);
|
||||
}
|
||||
}, 300);
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function NewPage() {
|
||||
const login = useLogin();
|
||||
const navigate = useNavigate();
|
||||
const login = useLogin();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [obj, setObj] = useState({
|
||||
name: "",
|
||||
desc: "",
|
||||
btih: "",
|
||||
tags: [] as Array<string>,
|
||||
files: [] as Array<{
|
||||
name: string;
|
||||
size: number;
|
||||
}>,
|
||||
const [obj, setObj] = useState({
|
||||
name: "",
|
||||
desc: "",
|
||||
btih: "",
|
||||
tags: [] as Array<string>,
|
||||
files: [] as Array<{
|
||||
name: string;
|
||||
size: number;
|
||||
}>,
|
||||
});
|
||||
|
||||
async function loadTorrent() {
|
||||
const f = await openFile();
|
||||
if (f) {
|
||||
const buf = await f.arrayBuffer();
|
||||
const torrent = bencode.decode(new Uint8Array(buf)) as Record<string, bencode.BencodeValue>;
|
||||
const infoBuf = bencode.encode(torrent["info"]);
|
||||
console.debug(torrent);
|
||||
const dec = new TextDecoder();
|
||||
const info = torrent["info"] as {
|
||||
files?: Array<{ length: number; path: Array<Uint8Array> }>;
|
||||
length: number;
|
||||
name: Uint8Array;
|
||||
};
|
||||
|
||||
setObj({
|
||||
name: dec.decode(info.name),
|
||||
desc: dec.decode(torrent["comment"] as Uint8Array | undefined) ?? "",
|
||||
btih: bytesToHex(sha1(infoBuf)),
|
||||
tags: [],
|
||||
files: (info.files ?? [{ length: info.length, path: [info.name] }]).map((a) => ({
|
||||
size: a.length,
|
||||
name: a.path.map((b) => dec.decode(b)).join("/"),
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function publish() {
|
||||
if (!login) return;
|
||||
const ev = await login.builder.generic((eb) => {
|
||||
const v = eb
|
||||
.kind(TorrentKind)
|
||||
.content(obj.desc)
|
||||
.tag(["title", obj.name])
|
||||
.tag(["size", String(obj.files.reduce((acc, v) => (acc += v.size), 0))])
|
||||
.tag(["btih", obj.btih]);
|
||||
|
||||
obj.tags.forEach((t) => v.tag(["t", t]));
|
||||
obj.files.forEach((f) => v.tag(["file", f.name, String(f.size)]));
|
||||
|
||||
return v;
|
||||
});
|
||||
console.debug(ev);
|
||||
|
||||
async function loadTorrent() {
|
||||
const f = await openFile();
|
||||
if (f) {
|
||||
const buf = await f.arrayBuffer();
|
||||
const torrent = bencode.decode(new Uint8Array(buf)) as Record<string, bencode.BencodeValue>;
|
||||
const infoBuf = bencode.encode(torrent["info"]);
|
||||
console.debug(torrent);
|
||||
const dec = new TextDecoder();
|
||||
const info = torrent["info"] as {
|
||||
files?: Array<{ length: number; path: Array<Uint8Array> }>;
|
||||
length: number;
|
||||
name: Uint8Array;
|
||||
};
|
||||
|
||||
setObj({
|
||||
name: dec.decode(info.name),
|
||||
desc: dec.decode(torrent["comment"] as Uint8Array | undefined) ?? "",
|
||||
btih: bytesToHex(sha1(infoBuf)),
|
||||
tags: [],
|
||||
files: (info.files ?? [{ length: info.length, path: [info.name] }]).map((a) => ({
|
||||
size: a.length,
|
||||
name: a.path.map(b => dec.decode(b)).join("/"),
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function publish() {
|
||||
if (!login) return;
|
||||
const ev = await login.builder.generic((eb) => {
|
||||
const v = eb
|
||||
.kind(TorrentKind)
|
||||
.content(obj.desc)
|
||||
.tag(["title", obj.name])
|
||||
.tag(["size", String(obj.files.reduce((acc, v) => (acc += v.size), 0))])
|
||||
.tag(["btih", obj.btih]);
|
||||
|
||||
obj.tags.forEach((t) => v.tag(["t", t]));
|
||||
obj.files.forEach((f) => v.tag(["file", f.name, String(f.size)]));
|
||||
|
||||
return v;
|
||||
});
|
||||
console.debug(ev);
|
||||
|
||||
if (ev) {
|
||||
await login.system.BroadcastEvent(ev);
|
||||
}
|
||||
navigate("/")
|
||||
}
|
||||
|
||||
function renderCategories(a: Category, tags: Array<string>): ReactNode {
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-1 bg-slate-500 p-1 rounded">
|
||||
<input
|
||||
type="radio"
|
||||
value={tags.join(",")}
|
||||
name="category"
|
||||
checked={obj.tags.join(",") === tags.join(",")}
|
||||
onChange={(e) =>
|
||||
setObj((o) => ({
|
||||
...o,
|
||||
tags: e.target.checked ? dedupe(e.target.value.split(",")) : [],
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<label>{a?.name}</label>
|
||||
</div>
|
||||
{a.sub_category?.map((b) => renderCategories(b, [...tags, b.tag]))}
|
||||
</>
|
||||
);
|
||||
if (ev) {
|
||||
await login.system.BroadcastEvent(ev);
|
||||
}
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
function renderCategories(a: Category, tags: Array<string>): ReactNode {
|
||||
return (
|
||||
<>
|
||||
<h1>New</h1>
|
||||
<div className="flex gap-1">
|
||||
<Button onClick={loadTorrent}>Import from Torrent</Button>
|
||||
<Button>Import from Magnet</Button>
|
||||
</div>
|
||||
<h2>Torrent Info</h2>
|
||||
<form className="flex flex-col gap-2">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1 flex flex-col gap-1">
|
||||
<label>Title</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="raw noods"
|
||||
value={obj.name}
|
||||
onChange={(e) => setObj((o) => ({ ...o, name: e.target.value }))}
|
||||
/>
|
||||
<label>Info Hash</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="hex"
|
||||
value={obj.btih}
|
||||
onChange={(e) => setObj((o) => ({ ...o, btih: e.target.value }))}
|
||||
/>
|
||||
<label>Category</label>
|
||||
<div className="flex flex-col gap-1">
|
||||
{Categories.map((a) => (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="font-bold bg-slate-800 p-1">{a.name}</div>
|
||||
<div className="flex gap-1 flex-wrap">{renderCategories(a, [a.tag])}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-1">
|
||||
<label>Description</label>
|
||||
<textarea
|
||||
rows={20}
|
||||
className="font-mono text-xs"
|
||||
value={obj.desc}
|
||||
onChange={(e) => setObj((o) => ({ ...o, desc: e.target.value }))}
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Files</h2>
|
||||
<div className="flex flex-col gap-2">
|
||||
{obj.files.map((a, i) => (
|
||||
<div className="flex gap-1">
|
||||
<input
|
||||
type="text"
|
||||
value={a.name}
|
||||
className="flex-1"
|
||||
placeholder="collection1/IMG_00001.jpg"
|
||||
onChange={(e) =>
|
||||
setObj((o) => ({
|
||||
...o,
|
||||
files: o.files.map((f, ii) => {
|
||||
if (ii === i) {
|
||||
return { ...f, name: e.target.value };
|
||||
}
|
||||
return f;
|
||||
}),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
value={a.size}
|
||||
min={0}
|
||||
placeholder="69000"
|
||||
onChange={(e) =>
|
||||
setObj((o) => ({
|
||||
...o,
|
||||
files: o.files.map((f, ii) => {
|
||||
if (ii === i) {
|
||||
return { ...f, size: Number(e.target.value) };
|
||||
}
|
||||
return f;
|
||||
}),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setObj((o) => ({
|
||||
...o,
|
||||
files: o.files.filter((_, ii) => i !== ii),
|
||||
}))
|
||||
}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setObj((o) => ({
|
||||
...o,
|
||||
files: [...o.files, { name: "", size: 0 }],
|
||||
}))
|
||||
}
|
||||
>
|
||||
Add File
|
||||
</Button>
|
||||
<Button onClick={publish}>Publish</Button>
|
||||
</form>
|
||||
</>
|
||||
<>
|
||||
<div className="flex gap-1 bg-slate-500 p-1 rounded">
|
||||
<input
|
||||
type="radio"
|
||||
value={tags.join(",")}
|
||||
name="category"
|
||||
checked={obj.tags.join(",") === tags.join(",")}
|
||||
onChange={(e) =>
|
||||
setObj((o) => ({
|
||||
...o,
|
||||
tags: e.target.checked ? dedupe(e.target.value.split(",")) : [],
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<label>{a?.name}</label>
|
||||
</div>
|
||||
{a.sub_category?.map((b) => renderCategories(b, [...tags, b.tag]))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>New</h1>
|
||||
<div className="flex gap-1">
|
||||
<Button onClick={loadTorrent}>Import from Torrent</Button>
|
||||
<Button>Import from Magnet</Button>
|
||||
</div>
|
||||
<h2>Torrent Info</h2>
|
||||
<form className="flex flex-col gap-2">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1 flex flex-col gap-1">
|
||||
<label>Title</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="raw noods"
|
||||
value={obj.name}
|
||||
onChange={(e) => setObj((o) => ({ ...o, name: e.target.value }))}
|
||||
/>
|
||||
<label>Info Hash</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="hex"
|
||||
value={obj.btih}
|
||||
onChange={(e) => setObj((o) => ({ ...o, btih: e.target.value }))}
|
||||
/>
|
||||
<label>Category</label>
|
||||
<div className="flex flex-col gap-1">
|
||||
{Categories.map((a) => (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="font-bold bg-slate-800 p-1">{a.name}</div>
|
||||
<div className="flex gap-1 flex-wrap">{renderCategories(a, [a.tag])}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-1">
|
||||
<label>Description</label>
|
||||
<textarea
|
||||
rows={20}
|
||||
className="font-mono text-xs"
|
||||
value={obj.desc}
|
||||
onChange={(e) => setObj((o) => ({ ...o, desc: e.target.value }))}
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Files</h2>
|
||||
<div className="flex flex-col gap-2">
|
||||
{obj.files.map((a, i) => (
|
||||
<div className="flex gap-1">
|
||||
<input
|
||||
type="text"
|
||||
value={a.name}
|
||||
className="flex-1"
|
||||
placeholder="collection1/IMG_00001.jpg"
|
||||
onChange={(e) =>
|
||||
setObj((o) => ({
|
||||
...o,
|
||||
files: o.files.map((f, ii) => {
|
||||
if (ii === i) {
|
||||
return { ...f, name: e.target.value };
|
||||
}
|
||||
return f;
|
||||
}),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
value={a.size}
|
||||
min={0}
|
||||
placeholder="69000"
|
||||
onChange={(e) =>
|
||||
setObj((o) => ({
|
||||
...o,
|
||||
files: o.files.map((f, ii) => {
|
||||
if (ii === i) {
|
||||
return { ...f, size: Number(e.target.value) };
|
||||
}
|
||||
return f;
|
||||
}),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setObj((o) => ({
|
||||
...o,
|
||||
files: o.files.filter((_, ii) => i !== ii),
|
||||
}))
|
||||
}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setObj((o) => ({
|
||||
...o,
|
||||
files: [...o.files, { name: "", size: 0 }],
|
||||
}))
|
||||
}
|
||||
>
|
||||
Add File
|
||||
</Button>
|
||||
<Button onClick={publish}>Publish</Button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
28
src/page/search.tsx
Normal file
28
src/page/search.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { NoteCollection, RequestBuilder } from "@snort/system";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { TorrentKind } from "../const";
|
||||
import { useRequestBuilder } from "@snort/system-react";
|
||||
import { TorrentList } from "../element/torrent-list";
|
||||
import { Search } from "../element/search";
|
||||
|
||||
export function SearchPage() {
|
||||
const params = useParams();
|
||||
const term = params.term as string | undefined;
|
||||
|
||||
const rb = new RequestBuilder(`search:${term}`);
|
||||
rb.withFilter()
|
||||
.kinds([TorrentKind])
|
||||
.search(term)
|
||||
.limit(100)
|
||||
.relay(["wss://relay.nostr.band", "wss://relay.noswhere.com"]);
|
||||
|
||||
const data = useRequestBuilder(NoteCollection, rb);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Search />
|
||||
<h2>Search Results:</h2>
|
||||
<TorrentList items={data.data ?? []} />
|
||||
</>
|
||||
);
|
||||
}
|
@ -30,8 +30,8 @@ export function TorrentDetail({ item }: { item: TaggedNostrEvent }) {
|
||||
const navigate = useNavigate();
|
||||
const name = item.tags.find((a) => a[0] === "title")?.at(1);
|
||||
const size = Number(item.tags.find((a) => a[0] === "size")?.at(1));
|
||||
const files = item.tags.filter(a => a[0] === "file");
|
||||
const tags = item.tags.filter(a => a[0] === "t").map(a => a[1]);
|
||||
const files = item.tags.filter((a) => a[0] === "file");
|
||||
const tags = item.tags.filter((a) => a[0] === "t").map((a) => a[1]);
|
||||
|
||||
async function deleteTorrent() {
|
||||
const ev = await login?.builder?.delete(item.id);
|
||||
@ -50,9 +50,13 @@ export function TorrentDetail({ item }: { item: TaggedNostrEvent }) {
|
||||
<div className="flex flex-col gap-1 bg-slate-700 p-2 rounded">
|
||||
<div>Size: {FormatBytes(size)}</div>
|
||||
<div>Uploaded: {new Date(item.created_at * 1000).toLocaleDateString()}</div>
|
||||
<div className="flex items-center gap-2">Tags: <div className="flex gap-1">
|
||||
{tags.map(a => <div className="rounded p-1 bg-slate-400">#{a}</div>)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
Tags:{" "}
|
||||
<div className="flex gap-1">
|
||||
{tags.map((a) => (
|
||||
<div className="rounded p-1 bg-slate-400">#{a}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<MagnetLink item={item} className="flex gap-1 items-center">
|
||||
@ -64,14 +68,18 @@ export function TorrentDetail({ item }: { item: TaggedNostrEvent }) {
|
||||
<pre className="font-mono text-xs bg-slate-700 p-2 rounded overflow-y-auto">{item.content}</pre>
|
||||
<h3>Files</h3>
|
||||
<div className="flex flex-col gap-1 bg-slate-700 p-2 rounded">
|
||||
{files.map(a => <div className="flex items-center gap-2">
|
||||
{a[1]}
|
||||
<small className="text-slate-500 font-semibold">{FormatBytes(Number(a[2]))}</small>
|
||||
</div>)}
|
||||
{files.map((a) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{a[1]}
|
||||
<small className="text-slate-500 font-semibold">{FormatBytes(Number(a[2]))}</small>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{item.pubkey == login?.publicKey && <Button className="bg-red-600 hover:bg-red-800" onClick={deleteTorrent}>
|
||||
Delete
|
||||
</Button>}
|
||||
{item.pubkey == login?.publicKey && (
|
||||
<Button className="bg-red-600 hover:bg-red-800" onClick={deleteTorrent}>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user