feat: stupid search

This commit is contained in:
Kieran 2023-11-27 15:12:45 +00:00
parent 1d16d61ea1
commit acd4c8ec3f
No known key found for this signature in database
GPG Key ID: DE71CEB3925BE941
7 changed files with 303 additions and 239 deletions

View File

@ -1,6 +1,16 @@
import { useNavigate, useParams } from "react-router-dom";
import { Categories } from "../const"; import { Categories } from "../const";
import { useEffect, useState } from "react";
export function Search() { export function Search() {
const params = useParams();
const navigate = useNavigate();
const [term, setTerm] = useState("");
useEffect(() => {
setTerm(params.term ?? "");
}, [params.term]);
return ( return (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<div className="flex gap-3 flex-wrap"> <div className="flex gap-3 flex-wrap">
@ -11,7 +21,18 @@ export function Search() {
</div> </div>
))} ))}
</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> </div>
); );
} }

View File

@ -21,7 +21,7 @@ export function TorrentList({ items }: { items: Array<TaggedNostrEvent> }) {
</thead> </thead>
<tbody> <tbody>
{items.map((a) => ( {items.map((a) => (
<TorrentTableEntry item={a} /> <TorrentTableEntry item={a} key={a.id} />
))} ))}
</tbody> </tbody>
</table> </table>

View File

@ -42,9 +42,11 @@ export function useLogin() {
() => LoginState.snapshot(), () => LoginState.snapshot(),
); );
const system = useContext(SnortContext); const system = useContext(SnortContext);
return session ? { return session
...session, ? {
builder: new EventPublisher(new Nip7Signer(), session.publicKey), ...session,
system builder: new EventPublisher(new Nip7Signer(), session.publicKey),
} : undefined; system,
}
: undefined;
} }

View File

@ -10,10 +10,11 @@ import { ProfilePage } from "./page/profile";
import { NewPage } from "./page/new"; import { NewPage } from "./page/new";
import { TorrentPage } from "./page/torrent"; import { TorrentPage } from "./page/torrent";
import { SnortSystemDb } from "@snort/system-web"; import { SnortSystemDb } from "@snort/system-web";
import { SearchPage } from "./page/search";
const db = new SnortSystemDb(); const db = new SnortSystemDb();
const System = new NostrSystem({ const System = new NostrSystem({
db db,
}); });
const Routes = [ const Routes = [
{ {
@ -42,6 +43,10 @@ const Routes = [
path: "/e/:id", path: "/e/:id",
element: <TorrentPage />, element: <TorrentPage />,
}, },
{
path: "/search/:term?",
element: <SearchPage />,
},
], ],
}, },
] as Array<RouteObject>; ] as Array<RouteObject>;

View File

@ -9,234 +9,234 @@ import { bytesToHex } from "@noble/hashes/utils";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
async function openFile(): Promise<File | undefined> { async function openFile(): Promise<File | undefined> {
return new Promise((resolve) => { return new Promise((resolve) => {
const elm = document.createElement("input"); const elm = document.createElement("input");
let lock = false; let lock = false;
elm.type = "file"; elm.type = "file";
elm.accept = ".torrent"; elm.accept = ".torrent";
const handleInput = (e: Event) => { const handleInput = (e: Event) => {
lock = true; lock = true;
const elm = e.target as HTMLInputElement; const elm = e.target as HTMLInputElement;
if ((elm.files?.length ?? 0) > 0) { if ((elm.files?.length ?? 0) > 0) {
resolve(elm.files![0]); resolve(elm.files![0]);
} else { } else {
resolve(undefined); resolve(undefined);
} }
}; };
elm.onchange = (e) => handleInput(e); elm.onchange = (e) => handleInput(e);
elm.click(); elm.click();
window.addEventListener( window.addEventListener(
"focus", "focus",
() => { () => {
setTimeout(() => { setTimeout(() => {
if (!lock) { if (!lock) {
console.debug("FOCUS WINDOW UPLOAD"); console.debug("FOCUS WINDOW UPLOAD");
resolve(undefined); resolve(undefined);
} }
}, 300); }, 300);
}, },
{ once: true }, { once: true },
); );
}); });
} }
export function NewPage() { export function NewPage() {
const login = useLogin(); const login = useLogin();
const navigate = useNavigate(); const navigate = useNavigate();
const [obj, setObj] = useState({ const [obj, setObj] = useState({
name: "", name: "",
desc: "", desc: "",
btih: "", btih: "",
tags: [] as Array<string>, tags: [] as Array<string>,
files: [] as Array<{ files: [] as Array<{
name: string; name: string;
size: number; 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() { if (ev) {
const f = await openFile(); await login.system.BroadcastEvent(ev);
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]))}
</>
);
} }
navigate("/");
}
function renderCategories(a: Category, tags: Array<string>): ReactNode {
return ( return (
<> <>
<h1>New</h1> <div className="flex gap-1 bg-slate-500 p-1 rounded">
<div className="flex gap-1"> <input
<Button onClick={loadTorrent}>Import from Torrent</Button> type="radio"
<Button>Import from Magnet</Button> value={tags.join(",")}
</div> name="category"
<h2>Torrent Info</h2> checked={obj.tags.join(",") === tags.join(",")}
<form className="flex flex-col gap-2"> onChange={(e) =>
<div className="flex gap-2"> setObj((o) => ({
<div className="flex-1 flex flex-col gap-1"> ...o,
<label>Title</label> tags: e.target.checked ? dedupe(e.target.value.split(",")) : [],
<input }))
type="text" }
placeholder="raw noods" />
value={obj.name} <label>{a?.name}</label>
onChange={(e) => setObj((o) => ({ ...o, name: e.target.value }))} </div>
/> {a.sub_category?.map((b) => renderCategories(b, [...tags, b.tag]))}
<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>
</>
); );
}
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
View 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 ?? []} />
</>
);
}

View File

@ -30,8 +30,8 @@ export function TorrentDetail({ item }: { item: TaggedNostrEvent }) {
const navigate = useNavigate(); const navigate = useNavigate();
const name = item.tags.find((a) => a[0] === "title")?.at(1); const name = item.tags.find((a) => a[0] === "title")?.at(1);
const size = Number(item.tags.find((a) => a[0] === "size")?.at(1)); const size = Number(item.tags.find((a) => a[0] === "size")?.at(1));
const files = item.tags.filter(a => a[0] === "file"); const files = item.tags.filter((a) => a[0] === "file");
const tags = item.tags.filter(a => a[0] === "t").map(a => a[1]); const tags = item.tags.filter((a) => a[0] === "t").map((a) => a[1]);
async function deleteTorrent() { async function deleteTorrent() {
const ev = await login?.builder?.delete(item.id); 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 className="flex flex-col gap-1 bg-slate-700 p-2 rounded">
<div>Size: {FormatBytes(size)}</div> <div>Size: {FormatBytes(size)}</div>
<div>Uploaded: {new Date(item.created_at * 1000).toLocaleDateString()}</div> <div>Uploaded: {new Date(item.created_at * 1000).toLocaleDateString()}</div>
<div className="flex items-center gap-2">Tags: <div className="flex gap-1"> <div className="flex items-center gap-2">
{tags.map(a => <div className="rounded p-1 bg-slate-400">#{a}</div>)} Tags:{" "}
</div> <div className="flex gap-1">
{tags.map((a) => (
<div className="rounded p-1 bg-slate-400">#{a}</div>
))}
</div>
</div> </div>
<div> <div>
<MagnetLink item={item} className="flex gap-1 items-center"> <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> <pre className="font-mono text-xs bg-slate-700 p-2 rounded overflow-y-auto">{item.content}</pre>
<h3>Files</h3> <h3>Files</h3>
<div className="flex flex-col gap-1 bg-slate-700 p-2 rounded"> <div className="flex flex-col gap-1 bg-slate-700 p-2 rounded">
{files.map(a => <div className="flex items-center gap-2"> {files.map((a) => (
{a[1]} <div className="flex items-center gap-2">
<small className="text-slate-500 font-semibold">{FormatBytes(Number(a[2]))}</small> {a[1]}
</div>)} <small className="text-slate-500 font-semibold">{FormatBytes(Number(a[2]))}</small>
</div>
))}
</div> </div>
{item.pubkey == login?.publicKey && <Button className="bg-red-600 hover:bg-red-800" onClick={deleteTorrent}> {item.pubkey == login?.publicKey && (
Delete <Button className="bg-red-600 hover:bg-red-800" onClick={deleteTorrent}>
</Button>} Delete
</Button>
)}
</div> </div>
); );
} }