feat: hide zero-seeder results with z key (#49)

This commit is contained in:
Felix-Ayush
2026-07-06 23:25:57 -04:00
committed by GitHub
parent 28565a88dd
commit 61d62893c8
4 changed files with 71 additions and 4 deletions
+22 -4
View File
@@ -9,6 +9,7 @@ import { useConcurrentSearch } from "../hooks/useConcurrentSearch";
import { getSource, SOURCES } from "../../sources/registry";
import { wrapStep, windowStart, resultsPanelOuter } from "../move";
import { sortResults, nextSort, sortLabel, sortArrow, type Sort, type SortField } from "../sort";
import { filterResults } from "../filter";
import { COLOR, GUTTER, ICON, sourceStyle } from "../theme";
import { cleanText, formatBytes, formatCount, formatRelative, stripControl, truncate } from "../../util/format";
import type { Source, TorrentResult } from "../../sources/types";
@@ -126,13 +127,14 @@ export function Results() {
const search = useConcurrentSearch(query);
const [sort, setSort] = useState<Sort>("none");
const [hideDead, setHideDead] = useState(false);
const results = useMemo(() => {
const cat = CATEGORIES.find((c) => c.key === section);
const base = cat?.group
? search.results.filter((r) => getSource(r.source).group === cat.group)
: search.results;
return sortResults(base, sort);
}, [search.results, section, sort]);
return sortResults(filterResults(base, hideDead), sort);
}, [search.results, section, sort, hideDead]);
const focused = region === "content";
const [mode, setMode] = useState<Mode>("list");
@@ -213,6 +215,8 @@ export function Results() {
if (r) copyResultMagnet(r);
} else if (input === "s") {
setSort((cur) => nextSort(cur));
} else if (input === "z") {
setHideDead((on) => !on);
}
},
{ isActive: focused && mode === "list" },
@@ -265,11 +269,12 @@ export function Results() {
};
const sortNote = sort === "none" ? "" : ` ${ICON.dot} sort: ${sortLabel(sort)}`;
const filterNote = hideDead ? ` ${ICON.dot} alive only` : "";
const status = () => {
if (search.loading) {
if (results.length > 0)
return <Text dimColor>{`searching… ${search.done}/${search.total} sources${sortNote}`}</Text>;
return <Text dimColor>{`searching… ${search.done}/${search.total} sources${sortNote}${filterNote}`}</Text>;
return (
<Spinner label={`${browsing ? "Loading" : "Searching"} ${search.done}/${search.total} sources`} />
);
@@ -292,6 +297,19 @@ export function Results() {
</Text>
);
}
if (hideDead) {
const cat = CATEGORIES.find((c) => c.key === section);
const base = cat?.group
? search.results.filter((r) => getSource(r.source).group === cat.group)
: search.results;
if (base.length > 0 && base.every((r) => r.seeders <= 0)) {
return (
<Text dimColor>
All results have zero seeders. Press z to show them.
</Text>
);
}
}
if (search.results.length > 0 && activeCat?.group)
return <Text dimColor>{`No ${activeCat.label.toLowerCase()} results yet. Try another tab or a search.`}</Text>;
return (
@@ -304,7 +322,7 @@ export function Results() {
const head = browsing
? "newest across all sources"
: `${results.length} result${results.length === 1 ? "" : "s"}`;
return <Text dimColor>{`${head}${note}${sortNote}`}</Text>;
return <Text dimColor>{`${head}${note}${sortNote}${filterNote}`}</Text>;
};
const sortMark = (field: SortField, label: string): ReactNode => {
+38
View File
@@ -0,0 +1,38 @@
import { describe, it, expect } from "vitest";
import { filterResults } from "./filter";
import type { SourceId, TorrentResult } from "../sources/types";
function r(p: Partial<TorrentResult> & { infoHash: string }): TorrentResult {
return {
name: p.name ?? p.infoHash,
sizeBytes: p.sizeBytes ?? 0,
seeders: p.seeders ?? 0,
leechers: p.leechers ?? 0,
source: (p.source ?? "yts") as SourceId,
magnet: p.magnet ?? `magnet:?xt=urn:btih:${p.infoHash}`,
...p,
};
}
describe("filterResults", () => {
it("passes everything through when hideDead is off", () => {
const list = [r({ infoHash: "a", seeders: 0 }), r({ infoHash: "b", seeders: 5 })];
expect(filterResults(list, false)).toEqual(list);
});
it("drops zero-seeder results when hideDead is on", () => {
const list = [
r({ infoHash: "a", seeders: 0 }),
r({ infoHash: "b", seeders: 1 }),
r({ infoHash: "c", seeders: 0 }),
];
expect(filterResults(list, true).map((x) => x.infoHash)).toEqual(["b"]);
});
it("does not mutate the input array", () => {
const list = [r({ infoHash: "a", seeders: 0 }), r({ infoHash: "b", seeders: 2 })];
const before = list.map((x) => x.infoHash);
filterResults(list, true);
expect(list.map((x) => x.infoHash)).toEqual(before);
});
});
+9
View File
@@ -0,0 +1,9 @@
import type { TorrentResult } from "../sources/types";
export function filterResults(
list: TorrentResult[],
hideDead: boolean,
): TorrentResult[] {
if (!hideDead) return list;
return list.filter((r) => r.seeders > 0);
}
+2
View File
@@ -31,6 +31,7 @@ export const HELP_GROUPS: HelpGroup[] = [
{ keys: "d", label: "Download" },
{ keys: "D", label: "Download to a folder…" },
{ keys: "s", label: "Sort results" },
{ keys: "z", label: "Hide dead torrents" },
{ keys: "y", label: "Copy magnet" },
{ keys: "m", label: "Paste magnet" },
],
@@ -113,6 +114,7 @@ export function footerHints(
{ keys: "d/D", label: "Download" },
{ keys: "y", label: "Copy" },
{ keys: "s", label: "Sort" },
{ keys: "z", label: "Alive" },
{ keys: "/", label: "Search" },
SWITCH,
ALWAYS,