mirror of
https://git.v0l.io/Kieran/dtan.git
synced 2024-12-12 15:06:22 +00:00
update for nip-35 spec
This commit is contained in:
parent
1479093aef
commit
85151ac008
22
package.json
22
package.json
@ -11,16 +11,16 @@
|
||||
"deploy": "yarn build && yarn dlx wrangler pages deploy dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^1.3.2",
|
||||
"@snort/shared": "^1.0.14",
|
||||
"@snort/system": "^1.2.12",
|
||||
"@snort/system-react": "^1.2.12",
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"@snort/shared": "^1.0.15",
|
||||
"@snort/system": "^1.3.2",
|
||||
"@snort/system-react": "^1.3.2",
|
||||
"@snort/system-wasm": "^1.0.2",
|
||||
"@snort/worker-relay": "^1.0.10",
|
||||
"classnames": "^2.3.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.20.0"
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.23.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.37",
|
||||
@ -34,10 +34,10 @@
|
||||
"eslint-plugin-react-refresh": "^0.4.4",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^3.1.0",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.1.6",
|
||||
"vite-plugin-pwa": "^0.19.4"
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.11",
|
||||
"vite-plugin-pwa": "^0.20.0"
|
||||
},
|
||||
"packageManager": "yarn@4.1.1",
|
||||
"prettier": {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import classNames from "classnames";
|
||||
import { HTMLProps, forwardRef, useState } from "react";
|
||||
|
||||
type ButtonProps = Omit<HTMLProps<HTMLButtonElement>, "onClick"> & {
|
||||
type ButtonProps = Omit<HTMLProps<HTMLButtonElement>, "onClick" | "small"> & {
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => Promise<void> | void;
|
||||
type: "primary" | "secondary" | "danger";
|
||||
small?: boolean;
|
||||
@ -34,7 +34,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) =>
|
||||
{...props}
|
||||
type="button"
|
||||
className={classNames(
|
||||
props.small ? "px-3 py-1 rounded-2xl" : "px-4 py-3 rounded-full ",
|
||||
props.small ? "px-3 py-1 rounded-2xl" : "px-4 py-3 rounded-xl ",
|
||||
"flex gap-1 items-center justify-center whitespace-nowrap",
|
||||
colorScheme,
|
||||
props.className,
|
||||
|
@ -33,3 +33,16 @@ a:not([href="/"], :has(button)) {
|
||||
.text {
|
||||
white-space-collapse: preserve-breaks;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
border: 0px;
|
||||
clip: rect(0px, 0px, 0px, 0px);
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
margin: -1px;
|
||||
padding: 0px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { unixNow } from "@snort/shared";
|
||||
import { NostrEvent, NotSignedNostrEvent } from "@snort/system";
|
||||
import { EventExt, NostrEvent, NotSignedNostrEvent } from "@snort/system";
|
||||
import { Trackers } from "./const";
|
||||
|
||||
export interface TorrentFile {
|
||||
@ -8,13 +8,13 @@ export interface TorrentFile {
|
||||
}
|
||||
|
||||
export interface TorrentTag {
|
||||
readonly type: "tcat" | "newznab" | "tmdb" | "ttvdb" | "imdb" | "mal" | "anilist" | undefined;
|
||||
readonly type: "tcat" | "newznab" | "tmdb" | "ttvdb" | "imdb" | "mal" | "anilist" | "generic";
|
||||
readonly value: string;
|
||||
}
|
||||
|
||||
export class NostrTorrent {
|
||||
constructor(
|
||||
readonly id: string,
|
||||
readonly id: string | undefined,
|
||||
readonly title: string,
|
||||
readonly summary: string,
|
||||
readonly infoHash: string,
|
||||
@ -61,7 +61,7 @@ export class NostrTorrent {
|
||||
return tcat.split(",");
|
||||
} else {
|
||||
// v0: ordered tags before tcat proposal
|
||||
const regularTags = this.tags.filter((a) => a.type === undefined).slice(0, 3);
|
||||
const regularTags = this.tags.filter((a) => a.type === "generic").slice(0, 3);
|
||||
return regularTags.map((a) => a.value);
|
||||
}
|
||||
}
|
||||
@ -96,6 +96,35 @@ export class NostrTorrent {
|
||||
return `magnet:?${params}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL for a non-generic external reference tag ("i" tag)
|
||||
*/
|
||||
static externalDbLink(tag: TorrentTag) {
|
||||
const ts = tag.value.split(":");
|
||||
switch (tag.type) {
|
||||
case "imdb":
|
||||
return `https://www.imdb.com/title/${tag.value}/`;
|
||||
case "tmdb": {
|
||||
if (ts.length === 2) {
|
||||
return `https://www.themoviedb.org/${ts[0]}/${ts[1]}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "mal": {
|
||||
if (ts.length === 2) {
|
||||
return `https://myanimelist.net/${ts[0]}/${ts[1]}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "anilist": {
|
||||
if (ts.length === 2) {
|
||||
return `https://anilist.co/${ts[0]}/${ts[1]}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the nostr event for this torrent
|
||||
*/
|
||||
@ -108,7 +137,7 @@ export class NostrTorrent {
|
||||
pubkey: pubkey ?? "",
|
||||
tags: [
|
||||
["title", this.title],
|
||||
["i", this.infoHash],
|
||||
["x", this.infoHash],
|
||||
],
|
||||
} as NotSignedNostrEvent;
|
||||
|
||||
@ -118,10 +147,16 @@ export class NostrTorrent {
|
||||
for (const tracker of this.trackers) {
|
||||
ret.tags.push(["tracker", tracker]);
|
||||
}
|
||||
for (const tag of this.tags) {
|
||||
ret.tags.push(["t", `${tag.type !== undefined ? `${tag.type}:` : ""}${tag.value}`]);
|
||||
for (const tag of this.tags.filter((a) => a.type === "generic")) {
|
||||
ret.tags.push(["t", tag.value]);
|
||||
}
|
||||
for (const tag of this.tags.filter((a) => a.type !== "generic")) {
|
||||
ret.tags.push(["i", `${tag.type}:${tag.value}`]);
|
||||
}
|
||||
|
||||
if (ret.id === undefined) {
|
||||
ret.id = EventExt.createId(ret);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
@ -147,7 +182,7 @@ export class NostrTorrent {
|
||||
}
|
||||
// v0: btih tag
|
||||
case "btih":
|
||||
case "i": {
|
||||
case "x": {
|
||||
infoHash = t[1];
|
||||
break;
|
||||
}
|
||||
@ -162,14 +197,9 @@ export class NostrTorrent {
|
||||
trackers.push(t[1]);
|
||||
break;
|
||||
}
|
||||
case "t": {
|
||||
case "i": {
|
||||
const kSplit = t[1].split(":", 2);
|
||||
if (kSplit.length === 1) {
|
||||
tags.push({
|
||||
type: undefined,
|
||||
value: t[1],
|
||||
});
|
||||
} else {
|
||||
if (kSplit.length === 2) {
|
||||
tags.push({
|
||||
type: kSplit[0],
|
||||
value: kSplit[1],
|
||||
@ -177,8 +207,15 @@ export class NostrTorrent {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "t": {
|
||||
tags.push({
|
||||
type: "generic",
|
||||
value: t[1],
|
||||
} as TorrentTag);
|
||||
break;
|
||||
}
|
||||
// v0: imdb tag
|
||||
case "imdb": {
|
||||
// v0: imdb tag
|
||||
tags.push({
|
||||
type: "imdb",
|
||||
value: t[1],
|
||||
|
@ -1,16 +1,3 @@
|
||||
label.category input {
|
||||
border: 0px;
|
||||
clip: rect(0px, 0px, 0px, 0px);
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
margin: -1px;
|
||||
padding: 0px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
label.category div {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
padding: 4px 10px;
|
||||
|
239
src/page/new.tsx
239
src/page/new.tsx
@ -1,14 +1,15 @@
|
||||
import "./new.css";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { Categories, Category, TorrentKind } from "../const";
|
||||
import { Categories, Category } from "../const";
|
||||
import { Button } from "../element/button";
|
||||
import { useLogin } from "../login";
|
||||
import { dedupe } from "@snort/shared";
|
||||
import { dedupe, unixNow } from "@snort/shared";
|
||||
import * as bencode from "../bencode";
|
||||
import { sha1 } from "@noble/hashes/sha1";
|
||||
import { bytesToHex } from "@noble/hashes/utils";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { NostrLink } from "@snort/system";
|
||||
import { NostrTorrent, TorrentTag } from "../nostr-torrent";
|
||||
|
||||
async function openFile(): Promise<File | undefined> {
|
||||
return new Promise((resolve) => {
|
||||
@ -47,12 +48,13 @@ type TorrentEntry = {
|
||||
name: string;
|
||||
desc: string;
|
||||
btih: string;
|
||||
tags: Array<string>;
|
||||
tcat: string;
|
||||
files: Array<{
|
||||
name: string;
|
||||
size: number;
|
||||
}>;
|
||||
trackers: Array<string>;
|
||||
externalLabels: Array<TorrentTag>;
|
||||
};
|
||||
|
||||
function entryIsValid(entry: TorrentEntry) {
|
||||
@ -60,7 +62,7 @@ function entryIsValid(entry: TorrentEntry) {
|
||||
entry.name &&
|
||||
entry.btih &&
|
||||
entry.files.length > 0 &&
|
||||
entry.tags.length > 0 &&
|
||||
entry.tcat.length > 0 &&
|
||||
entry.files.every((f) => f.name.length > 0)
|
||||
);
|
||||
}
|
||||
@ -68,14 +70,18 @@ function entryIsValid(entry: TorrentEntry) {
|
||||
export function NewPage() {
|
||||
const login = useLogin();
|
||||
const navigate = useNavigate();
|
||||
const [newLabelType, setNewLabelType] = useState<TorrentTag["type"]>("imdb");
|
||||
const [newLabelSubType, setNewLabelSubType] = useState("");
|
||||
const [newLabelValue, setNewLabelValue] = useState("");
|
||||
|
||||
const [obj, setObj] = useState<TorrentEntry>({
|
||||
name: "",
|
||||
desc: "",
|
||||
btih: "",
|
||||
tags: [],
|
||||
tcat: "",
|
||||
files: [],
|
||||
trackers: [],
|
||||
externalLabels: [],
|
||||
});
|
||||
|
||||
async function loadTorrent() {
|
||||
@ -91,61 +97,72 @@ export function NewPage() {
|
||||
length: number;
|
||||
name: Uint8Array;
|
||||
};
|
||||
const annouce = dec.decode(torrent["announce"] as Uint8Array | undefined);
|
||||
const announceList = (torrent["announce-list"] as Array<Array<Uint8Array>> | undefined)?.map((a) =>
|
||||
dec.decode(a[0]),
|
||||
);
|
||||
|
||||
setObj({
|
||||
name: dec.decode(info.name),
|
||||
desc: dec.decode(torrent["comment"] as Uint8Array | undefined) ?? "",
|
||||
btih: bytesToHex(sha1(infoBuf)),
|
||||
tags: [],
|
||||
tcat: "",
|
||||
files: (info.files ?? [{ length: info.length, path: [info.name] }]).map((a) => ({
|
||||
size: a.length,
|
||||
name: a.path.map((b) => dec.decode(b)).join("/"),
|
||||
})),
|
||||
trackers: [],
|
||||
trackers: dedupe([annouce, ...(announceList ?? [])]),
|
||||
externalLabels: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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(["btih", obj.btih])
|
||||
.tag(["alt", `${obj.name}\nTorrent published on https://dtan.xyz`]);
|
||||
|
||||
obj.tags.forEach((t) => v.tag(["t", t]));
|
||||
obj.files.forEach((f) => v.tag(["file", f.name, String(f.size)]));
|
||||
|
||||
return v;
|
||||
});
|
||||
const torrent = new NostrTorrent(
|
||||
undefined,
|
||||
obj.name,
|
||||
obj.desc,
|
||||
obj.btih,
|
||||
unixNow(),
|
||||
obj.files,
|
||||
obj.trackers,
|
||||
obj.externalLabels.concat([
|
||||
{
|
||||
type: "tcat",
|
||||
value: obj.tcat,
|
||||
},
|
||||
]),
|
||||
);
|
||||
const ev = torrent.toEvent(login.publicKey);
|
||||
ev.tags.push(["alt", `${obj.name}\nTorrent published on https://dtan.xyz`]);
|
||||
console.debug(ev);
|
||||
|
||||
if (ev) {
|
||||
await login.system.BroadcastEvent(ev);
|
||||
const evSigned = await login.builder.signer.sign(ev);
|
||||
login.system.BroadcastEvent(evSigned);
|
||||
navigate(`/e/${NostrLink.fromEvent(evSigned).encode()}`);
|
||||
}
|
||||
navigate(`/e/${NostrLink.fromEvent(ev).encode()}`);
|
||||
}
|
||||
|
||||
function renderCategories(a: Category, tags: Array<string>): ReactNode {
|
||||
const tcat = tags.join(",");
|
||||
return (
|
||||
<>
|
||||
<label className="category">
|
||||
<input
|
||||
type="radio"
|
||||
value={tags.join(",")}
|
||||
value={tcat}
|
||||
name="category"
|
||||
checked={obj.tags.join(",") === tags.join(",")}
|
||||
checked={obj.tcat === tcat}
|
||||
onChange={(e) =>
|
||||
setObj((o) => ({
|
||||
...o,
|
||||
tags: e.target.checked ? dedupe(e.target.value.split(",")) : [],
|
||||
tcat: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<div data-checked={obj.tags.join(",") === tags.join(",")}>{a?.name}</div>
|
||||
<div data-checked={obj.tcat === tcat}>{a?.name}</div>
|
||||
</label>
|
||||
|
||||
{a.sub_category?.map((b) => renderCategories(b, [...tags, b.tag]))}
|
||||
@ -153,6 +170,42 @@ export function NewPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function externalDbLogo(type: TorrentTag["type"]) {
|
||||
switch (type) {
|
||||
case "imdb":
|
||||
return (
|
||||
<img
|
||||
className="h-8"
|
||||
title="IMDB"
|
||||
src="https://m.media-amazon.com/images/G/01/imdb/images-ANDW73HA/favicon_desktop_32x32._CB1582158068_.png"
|
||||
/>
|
||||
);
|
||||
case "tmdb":
|
||||
return (
|
||||
<img
|
||||
className="h-8"
|
||||
title="TheMovieDatabase"
|
||||
src="https://www.themoviedb.org/assets/2/favicon-32x32-543a21832c8931d3494a68881f6afcafc58e96c5d324345377f3197a37b367b5.png"
|
||||
/>
|
||||
);
|
||||
case "ttvdb":
|
||||
return <img className="h-8" title="TheTVDatabase" src="https://thetvdb.com/images/icon.png" />;
|
||||
case "mal":
|
||||
return (
|
||||
<img
|
||||
className="h-8"
|
||||
title="MyAnimeList"
|
||||
src="https://myanimelist.net/img/common/pwa/launcher-icon-0-75x.png"
|
||||
/>
|
||||
);
|
||||
case "anilist":
|
||||
return <img className="h-8" title="AniList" src="https://anilist.co/img/icons/favicon-32x32.png" />;
|
||||
case "newznab":
|
||||
return <img className="h-8" title="newznab" src="https://www.newznab.com/favicon.ico" />;
|
||||
}
|
||||
return <div className="border border-neutral-600 rounded-xl px-2">{type}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>New Torrent</h2>
|
||||
@ -208,6 +261,136 @@ export function NewPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-indigo-300">External Ids</label>
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
value={newLabelType}
|
||||
onChange={(e) => setNewLabelType(e.target.value as TorrentTag["type"])}
|
||||
className="p-4 rounded-xl bg-neutral-800 focus-visible:outline-none"
|
||||
>
|
||||
<option value="imdb">IMDB</option>
|
||||
<option value="newznab">newznab</option>
|
||||
<option value="tmdb">TMDB (TheMovieDatabase)</option>
|
||||
<option value="ttvdb">TTVDB (TheTVDatabase)</option>
|
||||
<option value="mal">MAL (MyAnimeList)</option>
|
||||
<option value="anilist">AniList</option>
|
||||
</select>
|
||||
{(() => {
|
||||
switch (newLabelType) {
|
||||
case "mal":
|
||||
case "anilist": {
|
||||
if (newLabelSubType !== "anime" && newLabelSubType !== "manga") {
|
||||
setNewLabelSubType("anime");
|
||||
}
|
||||
return (
|
||||
<select
|
||||
className="p-4 rounded-xl bg-neutral-800 focus-visible:outline-none"
|
||||
value={newLabelSubType}
|
||||
onChange={(e) => setNewLabelSubType(e.target.value)}
|
||||
>
|
||||
<option value="anime">Anime</option>
|
||||
<option value="manga">Manga</option>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
case "tmdb": {
|
||||
if (newLabelSubType !== "tv" && newLabelSubType !== "movie") {
|
||||
setNewLabelSubType("tv");
|
||||
}
|
||||
return (
|
||||
<select
|
||||
className="p-4 rounded-xl bg-neutral-800 focus-visible:outline-none"
|
||||
value={newLabelSubType}
|
||||
onChange={(e) => setNewLabelSubType(e.target.value)}
|
||||
>
|
||||
<option value="tv">TV</option>
|
||||
<option value="movie">Movie</option>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
case "ttvdb": {
|
||||
if (newLabelSubType !== "series" && newLabelSubType !== "movies") {
|
||||
setNewLabelSubType("series");
|
||||
}
|
||||
return (
|
||||
<select
|
||||
className="p-4 rounded-xl bg-neutral-800 focus-visible:outline-none"
|
||||
value={newLabelSubType}
|
||||
onChange={(e) => setNewLabelSubType(e.target.value)}
|
||||
>
|
||||
<option value="series">Series</option>
|
||||
<option value="movies">Movie</option>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
if (newLabelSubType != "") {
|
||||
setNewLabelSubType("");
|
||||
}
|
||||
}
|
||||
}
|
||||
})()}
|
||||
<input
|
||||
type="text"
|
||||
className="p-4 rounded-xl bg-neutral-800 focus-visible:outline-none font-mono text-sm"
|
||||
value={newLabelValue}
|
||||
onChange={(e) => setNewLabelValue(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
type="secondary"
|
||||
onClick={() => {
|
||||
const existing = obj.externalLabels.find((a) => a.type === newLabelType);
|
||||
if (!existing) {
|
||||
obj.externalLabels.push({
|
||||
type: newLabelType as TorrentTag["type"],
|
||||
value: `${newLabelSubType ? `${newLabelSubType}:` : ""}${newLabelValue}`,
|
||||
});
|
||||
setObj({ ...obj });
|
||||
setNewLabelValue("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{obj.externalLabels.map((a) => {
|
||||
const link = NostrTorrent.externalDbLink(a);
|
||||
return (
|
||||
<div className="flex justify-between bg-neutral-800 px-3 py-1 rounded-xl">
|
||||
<div className="flex gap-2 items-center">
|
||||
{externalDbLogo(a.type)}
|
||||
{link && (
|
||||
<>
|
||||
<Link to={link} target="_blank" className="text-indigo-400 hover:underline">
|
||||
{a.value}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
{!link && a.value}
|
||||
</div>
|
||||
<Button
|
||||
type="danger"
|
||||
small={true}
|
||||
onClick={() =>
|
||||
setObj((o) => {
|
||||
const idx = o.externalLabels.findIndex((b) => b.type === a.type);
|
||||
if (idx !== -1) {
|
||||
o.externalLabels.splice(idx, 1);
|
||||
}
|
||||
return { ...o };
|
||||
})
|
||||
}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-indigo-300">
|
||||
Files <span className="text-red-500">*</span>
|
||||
@ -251,7 +434,7 @@ export function NewPage() {
|
||||
/>
|
||||
<Button
|
||||
small
|
||||
type="secondary"
|
||||
type="danger"
|
||||
onClick={() =>
|
||||
setObj((o) => ({
|
||||
...o,
|
||||
|
@ -10,6 +10,7 @@ export function SearchPage() {
|
||||
const term = params.term as string | undefined;
|
||||
const q = new URLSearchParams(location.search ?? "");
|
||||
const tags = q.get("tags")?.split(",") ?? [];
|
||||
const iz = q.getAll("i");
|
||||
|
||||
const rb = new RequestBuilder(`search:${term}+${tags.join(",")}`);
|
||||
const f = rb
|
||||
@ -21,6 +22,9 @@ export function SearchPage() {
|
||||
if (tags.length > 0) {
|
||||
f.tag("t", tags);
|
||||
}
|
||||
if (iz.length > 0) {
|
||||
f.tag("i", iz);
|
||||
}
|
||||
|
||||
const data = useRequestBuilder(rb);
|
||||
|
||||
|
@ -56,15 +56,24 @@ export function TorrentDetail({ item }: { item: TaggedNostrEvent }) {
|
||||
<div className="flex items-center gap-2">
|
||||
Tags:{" "}
|
||||
<div className="flex gap-2">
|
||||
{torrent.tags
|
||||
.filter((a) => a.type === undefined)
|
||||
.map((a, i) => (
|
||||
<div key={i} className="rounded-2xl py-1 px-4 bg-indigo-800 hover:bg-indigo-700">
|
||||
<Link to={`/search/?tags=${a.value}`}>#{a.value}</Link>
|
||||
</div>
|
||||
))}
|
||||
{torrent.tags.map((a, i) => {
|
||||
if (a.type === "generic") {
|
||||
return (
|
||||
<div key={i} className="rounded-2xl py-1 px-4 bg-indigo-800 hover:bg-indigo-700">
|
||||
<Link to={`/search/?tags=${a.value}`}>#{a.value}</Link>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div key={i} className="rounded-2xl py-1 px-4 bg-indigo-800 hover:bg-indigo-700">
|
||||
<Link to={`/search/?i=${a.type}:${a.value}`}>#{a.value}</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{torrent.trackers.length > 0 && <div>Trackers: {torrent.trackers.length}</div>}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Link to={torrent.magnetLink}>
|
||||
|
Loading…
Reference in New Issue
Block a user