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"
|
"deploy": "yarn build && yarn dlx wrangler pages deploy dist"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/hashes": "^1.3.2",
|
"@noble/hashes": "^1.4.0",
|
||||||
"@snort/shared": "^1.0.14",
|
"@snort/shared": "^1.0.15",
|
||||||
"@snort/system": "^1.2.12",
|
"@snort/system": "^1.3.2",
|
||||||
"@snort/system-react": "^1.2.12",
|
"@snort/system-react": "^1.3.2",
|
||||||
"@snort/system-wasm": "^1.0.2",
|
"@snort/system-wasm": "^1.0.2",
|
||||||
"@snort/worker-relay": "^1.0.10",
|
"@snort/worker-relay": "^1.0.10",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^6.20.0"
|
"react-router-dom": "^6.23.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.37",
|
"@types/react": "^18.2.37",
|
||||||
@ -34,10 +34,10 @@
|
|||||||
"eslint-plugin-react-refresh": "^0.4.4",
|
"eslint-plugin-react-refresh": "^0.4.4",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"prettier": "^3.1.0",
|
"prettier": "^3.1.0",
|
||||||
"tailwindcss": "^3.3.5",
|
"tailwindcss": "^3.4.3",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.4.5",
|
||||||
"vite": "^5.1.6",
|
"vite": "^5.2.11",
|
||||||
"vite-plugin-pwa": "^0.19.4"
|
"vite-plugin-pwa": "^0.20.0"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.1.1",
|
"packageManager": "yarn@4.1.1",
|
||||||
"prettier": {
|
"prettier": {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { HTMLProps, forwardRef, useState } from "react";
|
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;
|
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => Promise<void> | void;
|
||||||
type: "primary" | "secondary" | "danger";
|
type: "primary" | "secondary" | "danger";
|
||||||
small?: boolean;
|
small?: boolean;
|
||||||
@ -34,7 +34,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) =>
|
|||||||
{...props}
|
{...props}
|
||||||
type="button"
|
type="button"
|
||||||
className={classNames(
|
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",
|
"flex gap-1 items-center justify-center whitespace-nowrap",
|
||||||
colorScheme,
|
colorScheme,
|
||||||
props.className,
|
props.className,
|
||||||
|
@ -33,3 +33,16 @@ a:not([href="/"], :has(button)) {
|
|||||||
.text {
|
.text {
|
||||||
white-space-collapse: preserve-breaks;
|
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 { unixNow } from "@snort/shared";
|
||||||
import { NostrEvent, NotSignedNostrEvent } from "@snort/system";
|
import { EventExt, NostrEvent, NotSignedNostrEvent } from "@snort/system";
|
||||||
import { Trackers } from "./const";
|
import { Trackers } from "./const";
|
||||||
|
|
||||||
export interface TorrentFile {
|
export interface TorrentFile {
|
||||||
@ -8,13 +8,13 @@ export interface TorrentFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TorrentTag {
|
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;
|
readonly value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NostrTorrent {
|
export class NostrTorrent {
|
||||||
constructor(
|
constructor(
|
||||||
readonly id: string,
|
readonly id: string | undefined,
|
||||||
readonly title: string,
|
readonly title: string,
|
||||||
readonly summary: string,
|
readonly summary: string,
|
||||||
readonly infoHash: string,
|
readonly infoHash: string,
|
||||||
@ -61,7 +61,7 @@ export class NostrTorrent {
|
|||||||
return tcat.split(",");
|
return tcat.split(",");
|
||||||
} else {
|
} else {
|
||||||
// v0: ordered tags before tcat proposal
|
// 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);
|
return regularTags.map((a) => a.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -96,6 +96,35 @@ export class NostrTorrent {
|
|||||||
return `magnet:?${params}`;
|
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
|
* Get the nostr event for this torrent
|
||||||
*/
|
*/
|
||||||
@ -108,7 +137,7 @@ export class NostrTorrent {
|
|||||||
pubkey: pubkey ?? "",
|
pubkey: pubkey ?? "",
|
||||||
tags: [
|
tags: [
|
||||||
["title", this.title],
|
["title", this.title],
|
||||||
["i", this.infoHash],
|
["x", this.infoHash],
|
||||||
],
|
],
|
||||||
} as NotSignedNostrEvent;
|
} as NotSignedNostrEvent;
|
||||||
|
|
||||||
@ -118,10 +147,16 @@ export class NostrTorrent {
|
|||||||
for (const tracker of this.trackers) {
|
for (const tracker of this.trackers) {
|
||||||
ret.tags.push(["tracker", tracker]);
|
ret.tags.push(["tracker", tracker]);
|
||||||
}
|
}
|
||||||
for (const tag of this.tags) {
|
for (const tag of this.tags.filter((a) => a.type === "generic")) {
|
||||||
ret.tags.push(["t", `${tag.type !== undefined ? `${tag.type}:` : ""}${tag.value}`]);
|
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;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,7 +182,7 @@ export class NostrTorrent {
|
|||||||
}
|
}
|
||||||
// v0: btih tag
|
// v0: btih tag
|
||||||
case "btih":
|
case "btih":
|
||||||
case "i": {
|
case "x": {
|
||||||
infoHash = t[1];
|
infoHash = t[1];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -162,14 +197,9 @@ export class NostrTorrent {
|
|||||||
trackers.push(t[1]);
|
trackers.push(t[1]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "t": {
|
case "i": {
|
||||||
const kSplit = t[1].split(":", 2);
|
const kSplit = t[1].split(":", 2);
|
||||||
if (kSplit.length === 1) {
|
if (kSplit.length === 2) {
|
||||||
tags.push({
|
|
||||||
type: undefined,
|
|
||||||
value: t[1],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
tags.push({
|
tags.push({
|
||||||
type: kSplit[0],
|
type: kSplit[0],
|
||||||
value: kSplit[1],
|
value: kSplit[1],
|
||||||
@ -177,8 +207,15 @@ export class NostrTorrent {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "imdb": {
|
case "t": {
|
||||||
|
tags.push({
|
||||||
|
type: "generic",
|
||||||
|
value: t[1],
|
||||||
|
} as TorrentTag);
|
||||||
|
break;
|
||||||
|
}
|
||||||
// v0: imdb tag
|
// v0: imdb tag
|
||||||
|
case "imdb": {
|
||||||
tags.push({
|
tags.push({
|
||||||
type: "imdb",
|
type: "imdb",
|
||||||
value: t[1],
|
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 {
|
label.category div {
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
|
239
src/page/new.tsx
239
src/page/new.tsx
@ -1,14 +1,15 @@
|
|||||||
import "./new.css";
|
import "./new.css";
|
||||||
import { ReactNode, useState } from "react";
|
import { ReactNode, useState } from "react";
|
||||||
import { Categories, Category, TorrentKind } from "../const";
|
import { Categories, Category } from "../const";
|
||||||
import { Button } from "../element/button";
|
import { Button } from "../element/button";
|
||||||
import { useLogin } from "../login";
|
import { useLogin } from "../login";
|
||||||
import { dedupe } from "@snort/shared";
|
import { dedupe, unixNow } from "@snort/shared";
|
||||||
import * as bencode from "../bencode";
|
import * as bencode from "../bencode";
|
||||||
import { sha1 } from "@noble/hashes/sha1";
|
import { sha1 } from "@noble/hashes/sha1";
|
||||||
import { bytesToHex } from "@noble/hashes/utils";
|
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 { NostrLink } from "@snort/system";
|
||||||
|
import { NostrTorrent, TorrentTag } from "../nostr-torrent";
|
||||||
|
|
||||||
async function openFile(): Promise<File | undefined> {
|
async function openFile(): Promise<File | undefined> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@ -47,12 +48,13 @@ type TorrentEntry = {
|
|||||||
name: string;
|
name: string;
|
||||||
desc: string;
|
desc: string;
|
||||||
btih: string;
|
btih: string;
|
||||||
tags: Array<string>;
|
tcat: string;
|
||||||
files: Array<{
|
files: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
size: number;
|
size: number;
|
||||||
}>;
|
}>;
|
||||||
trackers: Array<string>;
|
trackers: Array<string>;
|
||||||
|
externalLabels: Array<TorrentTag>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function entryIsValid(entry: TorrentEntry) {
|
function entryIsValid(entry: TorrentEntry) {
|
||||||
@ -60,7 +62,7 @@ function entryIsValid(entry: TorrentEntry) {
|
|||||||
entry.name &&
|
entry.name &&
|
||||||
entry.btih &&
|
entry.btih &&
|
||||||
entry.files.length > 0 &&
|
entry.files.length > 0 &&
|
||||||
entry.tags.length > 0 &&
|
entry.tcat.length > 0 &&
|
||||||
entry.files.every((f) => f.name.length > 0)
|
entry.files.every((f) => f.name.length > 0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -68,14 +70,18 @@ function entryIsValid(entry: TorrentEntry) {
|
|||||||
export function NewPage() {
|
export function NewPage() {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [newLabelType, setNewLabelType] = useState<TorrentTag["type"]>("imdb");
|
||||||
|
const [newLabelSubType, setNewLabelSubType] = useState("");
|
||||||
|
const [newLabelValue, setNewLabelValue] = useState("");
|
||||||
|
|
||||||
const [obj, setObj] = useState<TorrentEntry>({
|
const [obj, setObj] = useState<TorrentEntry>({
|
||||||
name: "",
|
name: "",
|
||||||
desc: "",
|
desc: "",
|
||||||
btih: "",
|
btih: "",
|
||||||
tags: [],
|
tcat: "",
|
||||||
files: [],
|
files: [],
|
||||||
trackers: [],
|
trackers: [],
|
||||||
|
externalLabels: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadTorrent() {
|
async function loadTorrent() {
|
||||||
@ -91,61 +97,72 @@ export function NewPage() {
|
|||||||
length: number;
|
length: number;
|
||||||
name: Uint8Array;
|
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({
|
setObj({
|
||||||
name: dec.decode(info.name),
|
name: dec.decode(info.name),
|
||||||
desc: dec.decode(torrent["comment"] as Uint8Array | undefined) ?? "",
|
desc: dec.decode(torrent["comment"] as Uint8Array | undefined) ?? "",
|
||||||
btih: bytesToHex(sha1(infoBuf)),
|
btih: bytesToHex(sha1(infoBuf)),
|
||||||
tags: [],
|
tcat: "",
|
||||||
files: (info.files ?? [{ length: info.length, path: [info.name] }]).map((a) => ({
|
files: (info.files ?? [{ length: info.length, path: [info.name] }]).map((a) => ({
|
||||||
size: a.length,
|
size: a.length,
|
||||||
name: a.path.map((b) => dec.decode(b)).join("/"),
|
name: a.path.map((b) => dec.decode(b)).join("/"),
|
||||||
})),
|
})),
|
||||||
trackers: [],
|
trackers: dedupe([annouce, ...(announceList ?? [])]),
|
||||||
|
externalLabels: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function publish() {
|
async function publish() {
|
||||||
if (!login) return;
|
if (!login) return;
|
||||||
const ev = await login.builder.generic((eb) => {
|
const torrent = new NostrTorrent(
|
||||||
const v = eb
|
undefined,
|
||||||
.kind(TorrentKind)
|
obj.name,
|
||||||
.content(obj.desc)
|
obj.desc,
|
||||||
.tag(["title", obj.name])
|
obj.btih,
|
||||||
.tag(["btih", obj.btih])
|
unixNow(),
|
||||||
.tag(["alt", `${obj.name}\nTorrent published on https://dtan.xyz`]);
|
obj.files,
|
||||||
|
obj.trackers,
|
||||||
obj.tags.forEach((t) => v.tag(["t", t]));
|
obj.externalLabels.concat([
|
||||||
obj.files.forEach((f) => v.tag(["file", f.name, String(f.size)]));
|
{
|
||||||
|
type: "tcat",
|
||||||
return v;
|
value: obj.tcat,
|
||||||
});
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const ev = torrent.toEvent(login.publicKey);
|
||||||
|
ev.tags.push(["alt", `${obj.name}\nTorrent published on https://dtan.xyz`]);
|
||||||
console.debug(ev);
|
console.debug(ev);
|
||||||
|
|
||||||
if (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 {
|
function renderCategories(a: Category, tags: Array<string>): ReactNode {
|
||||||
|
const tcat = tags.join(",");
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<label className="category">
|
<label className="category">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
value={tags.join(",")}
|
value={tcat}
|
||||||
name="category"
|
name="category"
|
||||||
checked={obj.tags.join(",") === tags.join(",")}
|
checked={obj.tcat === tcat}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setObj((o) => ({
|
setObj((o) => ({
|
||||||
...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>
|
</label>
|
||||||
|
|
||||||
{a.sub_category?.map((b) => renderCategories(b, [...tags, b.tag]))}
|
{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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<h2>New Torrent</h2>
|
<h2>New Torrent</h2>
|
||||||
@ -208,6 +261,136 @@ export function NewPage() {
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="flex flex-col gap-2">
|
||||||
<label className="text-indigo-300">
|
<label className="text-indigo-300">
|
||||||
Files <span className="text-red-500">*</span>
|
Files <span className="text-red-500">*</span>
|
||||||
@ -251,7 +434,7 @@ export function NewPage() {
|
|||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
small
|
small
|
||||||
type="secondary"
|
type="danger"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setObj((o) => ({
|
setObj((o) => ({
|
||||||
...o,
|
...o,
|
||||||
|
@ -10,6 +10,7 @@ export function SearchPage() {
|
|||||||
const term = params.term as string | undefined;
|
const term = params.term as string | undefined;
|
||||||
const q = new URLSearchParams(location.search ?? "");
|
const q = new URLSearchParams(location.search ?? "");
|
||||||
const tags = q.get("tags")?.split(",") ?? [];
|
const tags = q.get("tags")?.split(",") ?? [];
|
||||||
|
const iz = q.getAll("i");
|
||||||
|
|
||||||
const rb = new RequestBuilder(`search:${term}+${tags.join(",")}`);
|
const rb = new RequestBuilder(`search:${term}+${tags.join(",")}`);
|
||||||
const f = rb
|
const f = rb
|
||||||
@ -21,6 +22,9 @@ export function SearchPage() {
|
|||||||
if (tags.length > 0) {
|
if (tags.length > 0) {
|
||||||
f.tag("t", tags);
|
f.tag("t", tags);
|
||||||
}
|
}
|
||||||
|
if (iz.length > 0) {
|
||||||
|
f.tag("i", iz);
|
||||||
|
}
|
||||||
|
|
||||||
const data = useRequestBuilder(rb);
|
const data = useRequestBuilder(rb);
|
||||||
|
|
||||||
|
@ -56,15 +56,24 @@ export function TorrentDetail({ item }: { item: TaggedNostrEvent }) {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
Tags:{" "}
|
Tags:{" "}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{torrent.tags
|
{torrent.tags.map((a, i) => {
|
||||||
.filter((a) => a.type === undefined)
|
if (a.type === "generic") {
|
||||||
.map((a, i) => (
|
return (
|
||||||
<div key={i} className="rounded-2xl py-1 px-4 bg-indigo-800 hover:bg-indigo-700">
|
<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>
|
<Link to={`/search/?tags=${a.value}`}>#{a.value}</Link>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
{torrent.trackers.length > 0 && <div>Trackers: {torrent.trackers.length}</div>}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Link to={torrent.magnetLink}>
|
<Link to={torrent.magnetLink}>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user