mirror of
https://github.com/baairon/torlink.git
synced 2026-07-08 18:28:22 +02:00
feat: polish prompt ux and fix z hiding sources without seeder data
- new PromptHints action row shared by the folder and trackers prompts - download-to prompt shows the torrent name and size, enter reads "download" - export torrent rebound from T to s and centralized in the store - footer trimmed to essentials, d/D and z detail live in the ? sheet - z keeps FitGirl and SubsPlease rows: their feeds carry no seeder data, a new per-source reportsHealth flag marks seeders: 0 as unknown, not dead - version 1.3.0
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "torlnk",
|
"name": "torlnk",
|
||||||
"version": "1.1.1",
|
"version": "1.3.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "torlnk",
|
"name": "torlnk",
|
||||||
"version": "1.1.1",
|
"version": "1.3.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"env-paths": "^4.0.0",
|
"env-paths": "^4.0.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "torlnk",
|
"name": "torlnk",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"description": "A sleek, zero-setup torrent finder and downloader that lives right in your terminal.",
|
"description": "A sleek, zero-setup torrent finder and downloader that lives right in your terminal.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ function makeStore(
|
|||||||
requestDownloadTo: noop,
|
requestDownloadTo: noop,
|
||||||
copyMagnet: noop,
|
copyMagnet: noop,
|
||||||
openDownloadFolder: noop,
|
openDownloadFolder: noop,
|
||||||
|
exportTorrent: noop,
|
||||||
notice: null,
|
notice: null,
|
||||||
setNotice: noop,
|
setNotice: noop,
|
||||||
quitAll: noop,
|
quitAll: noop,
|
||||||
|
|||||||
@@ -54,5 +54,6 @@ export const eztv: Source = {
|
|||||||
label: "EZTV",
|
label: "EZTV",
|
||||||
group: "TV",
|
group: "TV",
|
||||||
homepage: "https://eztvx.to",
|
homepage: "https://eztvx.to",
|
||||||
|
reportsHealth: true,
|
||||||
search,
|
search,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,5 +8,7 @@ export const fitgirl: Source = {
|
|||||||
label: "FitGirl",
|
label: "FitGirl",
|
||||||
group: "Games",
|
group: "Games",
|
||||||
homepage: HOME,
|
homepage: HOME,
|
||||||
|
// WordPress RSS carries no swarm data; every result reports seeders: 0.
|
||||||
|
reportsHealth: false,
|
||||||
search: (query, opts) => fetchWordpressRss(HOME, "fitgirl", query, opts),
|
search: (query, opts) => fetchWordpressRss(HOME, "fitgirl", query, opts),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -46,5 +46,6 @@ export const nyaa: Source = {
|
|||||||
label: "Nyaa",
|
label: "Nyaa",
|
||||||
group: "Anime",
|
group: "Anime",
|
||||||
homepage: "https://nyaa.si",
|
homepage: "https://nyaa.si",
|
||||||
|
reportsHealth: true,
|
||||||
search,
|
search,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export const tpbMovies: Source = {
|
|||||||
label: "TPB",
|
label: "TPB",
|
||||||
group: "Movies",
|
group: "Movies",
|
||||||
homepage: "https://thepiratebay.org",
|
homepage: "https://thepiratebay.org",
|
||||||
|
reportsHealth: true,
|
||||||
search: (query, opts = {}) => search(query, MOVIE_CATS, TOP_MOVIES, "tpb-movies", opts),
|
search: (query, opts = {}) => search(query, MOVIE_CATS, TOP_MOVIES, "tpb-movies", opts),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -87,5 +88,6 @@ export const tpbTv: Source = {
|
|||||||
label: "TPB",
|
label: "TPB",
|
||||||
group: "TV",
|
group: "TV",
|
||||||
homepage: "https://thepiratebay.org",
|
homepage: "https://thepiratebay.org",
|
||||||
|
reportsHealth: true,
|
||||||
search: (query, opts = {}) => search(query, TV_CATS, TOP_TV, "tpb-tv", opts),
|
search: (query, opts = {}) => search(query, TV_CATS, TOP_TV, "tpb-tv", opts),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -71,5 +71,7 @@ export const subsplease: Source = {
|
|||||||
label: "SubsPlease",
|
label: "SubsPlease",
|
||||||
group: "Anime",
|
group: "Anime",
|
||||||
homepage: "https://subsplease.org",
|
homepage: "https://subsplease.org",
|
||||||
|
// The SubsPlease API has no swarm data; every result reports seeders: 0.
|
||||||
|
reportsHealth: false,
|
||||||
search,
|
search,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,5 +32,9 @@ export interface Source {
|
|||||||
label: string;
|
label: string;
|
||||||
group: SourceGroup;
|
group: SourceGroup;
|
||||||
homepage: string;
|
homepage: string;
|
||||||
|
// True when the source returns real swarm counts. False when its feed has
|
||||||
|
// none, so seeders: 0 means unknown, not dead (the alive-only filter must
|
||||||
|
// never drop those rows).
|
||||||
|
reportsHealth: boolean;
|
||||||
search(query: string, opts?: SearchOptions): Promise<TorrentResult[]>;
|
search(query: string, opts?: SearchOptions): Promise<TorrentResult[]>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ export const x1337Movies: Source = {
|
|||||||
label: "1337x",
|
label: "1337x",
|
||||||
group: "Movies",
|
group: "Movies",
|
||||||
homepage: "https://1337x.to",
|
homepage: "https://1337x.to",
|
||||||
|
reportsHealth: true,
|
||||||
search: (query, opts = {}) => search(query, "Movies", "x1337-movies", opts),
|
search: (query, opts = {}) => search(query, "Movies", "x1337-movies", opts),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -150,5 +151,6 @@ export const x1337Tv: Source = {
|
|||||||
label: "1337x",
|
label: "1337x",
|
||||||
group: "TV",
|
group: "TV",
|
||||||
homepage: "https://1337x.to",
|
homepage: "https://1337x.to",
|
||||||
|
reportsHealth: true,
|
||||||
search: (query, opts = {}) => search(query, "TV", "x1337-tv", opts),
|
search: (query, opts = {}) => search(query, "TV", "x1337-tv", opts),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -76,5 +76,6 @@ export const yts: Source = {
|
|||||||
label: "YTS",
|
label: "YTS",
|
||||||
group: "Movies",
|
group: "Movies",
|
||||||
homepage: "https://yts.mx",
|
homepage: "https://yts.mx",
|
||||||
|
reportsHealth: true,
|
||||||
search,
|
search,
|
||||||
};
|
};
|
||||||
|
|||||||
+24
-1
@@ -11,7 +11,7 @@ import { parseInput } from "../sources/magnet";
|
|||||||
import { magnetFromTorrentFile } from "../sources/torrentFile";
|
import { magnetFromTorrentFile } from "../sources/torrentFile";
|
||||||
import { readClipboard, writeClipboard } from "../util/clipboard";
|
import { readClipboard, writeClipboard } from "../util/clipboard";
|
||||||
import { openFolder } from "../util/openFolder";
|
import { openFolder } from "../util/openFolder";
|
||||||
import { cleanText, truncate } from "../util/format";
|
import { cleanText, formatBytes, truncate } from "../util/format";
|
||||||
import {
|
import {
|
||||||
StoreContext,
|
StoreContext,
|
||||||
type CaptureMode,
|
type CaptureMode,
|
||||||
@@ -307,6 +307,21 @@ export function App({
|
|||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const exportTorrent = useCallback(
|
||||||
|
(input: { id: string; name: string }) => {
|
||||||
|
if (!queue) return;
|
||||||
|
void (async () => {
|
||||||
|
const file = await queue.exportTorrentFile(input.id);
|
||||||
|
if (file) {
|
||||||
|
setNotice(`Exported torrent file: ${truncate(file, 48)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setNotice(`No torrent file yet for ${truncate(cleanText(input.name), 32)}.`);
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
[queue],
|
||||||
|
);
|
||||||
|
|
||||||
const submitQuery = useCallback(
|
const submitQuery = useCallback(
|
||||||
(raw: string) => {
|
(raw: string) => {
|
||||||
const q = raw.trim();
|
const q = raw.trim();
|
||||||
@@ -389,6 +404,7 @@ export function App({
|
|||||||
requestDownloadTo,
|
requestDownloadTo,
|
||||||
copyMagnet,
|
copyMagnet,
|
||||||
openDownloadFolder,
|
openDownloadFolder,
|
||||||
|
exportTorrent,
|
||||||
notice,
|
notice,
|
||||||
setNotice,
|
setNotice,
|
||||||
quitAll,
|
quitAll,
|
||||||
@@ -417,6 +433,7 @@ export function App({
|
|||||||
requestDownloadTo,
|
requestDownloadTo,
|
||||||
copyMagnet,
|
copyMagnet,
|
||||||
openDownloadFolder,
|
openDownloadFolder,
|
||||||
|
exportTorrent,
|
||||||
notice,
|
notice,
|
||||||
listRows,
|
listRows,
|
||||||
compact,
|
compact,
|
||||||
@@ -546,6 +563,12 @@ export function App({
|
|||||||
<FolderPrompt
|
<FolderPrompt
|
||||||
title="download to"
|
title="download to"
|
||||||
width={Math.max(24, Math.min(cols - 4, 62))}
|
width={Math.max(24, Math.min(cols - 4, 62))}
|
||||||
|
subject={
|
||||||
|
pendingDownload.sizeBytes
|
||||||
|
? `${cleanText(pendingDownload.name)} ${ICON.dot} ${formatBytes(pendingDownload.sizeBytes)}`
|
||||||
|
: cleanText(pendingDownload.name)
|
||||||
|
}
|
||||||
|
submitLabel="download"
|
||||||
value={lastDownloadToDir ?? store.config.downloadDir}
|
value={lastDownloadToDir ?? store.config.downloadDir}
|
||||||
onSubmit={startDownloadTo}
|
onSubmit={startDownloadTo}
|
||||||
onCancel={closeDownloadToPrompt}
|
onCancel={closeDownloadToPrompt}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export function Downloads() {
|
|||||||
startDownload,
|
startDownload,
|
||||||
openDownloadFolder,
|
openDownloadFolder,
|
||||||
setDownloadFocus,
|
setDownloadFocus,
|
||||||
setNotice,
|
exportTorrent,
|
||||||
} = useStore();
|
} = useStore();
|
||||||
const active = useQueueItems(queue);
|
const active = useQueueItems(queue);
|
||||||
const recent = useQueueHistory(queue);
|
const recent = useQueueHistory(queue);
|
||||||
@@ -73,14 +73,9 @@ export function Downloads() {
|
|||||||
else if (input === "e") {
|
else if (input === "e") {
|
||||||
const dir = inActive ? active[clamped]?.dir : recent[recentCursor]?.dir;
|
const dir = inActive ? active[clamped]?.dir : recent[recentCursor]?.dir;
|
||||||
if (dir) openDownloadFolder(dir);
|
if (dir) openDownloadFolder(dir);
|
||||||
} else if (input === "T") {
|
} else if (input === "s") {
|
||||||
const item = inActive ? active[clamped] : recent[recentCursor];
|
const item = inActive ? active[clamped] : recent[recentCursor];
|
||||||
if (!item) return;
|
if (item) exportTorrent({ id: item.id, name: item.name });
|
||||||
void (async () => {
|
|
||||||
const file = await queue.exportTorrentFile(item.id);
|
|
||||||
if (file) setNotice(`Exported .torrent: ${truncate(file, 48)}`);
|
|
||||||
else setNotice(`No .torrent metadata yet for ${truncate(cleanText(item.name), 32)}.`);
|
|
||||||
})();
|
|
||||||
} else if (inActive) {
|
} else if (inActive) {
|
||||||
const it = active[clamped];
|
const it = active[clamped];
|
||||||
if (!it) return;
|
if (!it) return;
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
import { Box, Text, useInput } from "ink";
|
import { Box, Text, useInput } from "ink";
|
||||||
import { TextField } from "./TextField";
|
import { TextField } from "./TextField";
|
||||||
import { Panel } from "./Panel";
|
import { Panel } from "./Panel";
|
||||||
|
import { PromptHints } from "./PromptHints";
|
||||||
import { COLOR, ICON } from "../theme";
|
import { COLOR, ICON } from "../theme";
|
||||||
|
|
||||||
interface FolderPromptProps {
|
interface FolderPromptProps {
|
||||||
width: number;
|
width: number;
|
||||||
value: string;
|
value: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
// One dim context line above the input, e.g. the torrent this download is
|
||||||
|
// for. Without it the prompt is a bare path box with no hint of what the
|
||||||
|
// user is placing.
|
||||||
|
subject?: string;
|
||||||
|
// Verb next to ↵; the default fits the settings prompt, the per-download
|
||||||
|
// prompt passes "download" so enter reads as the action it performs.
|
||||||
|
submitLabel?: string;
|
||||||
onSubmit: (value: string) => void;
|
onSubmit: (value: string) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
@@ -14,7 +22,9 @@ interface FolderPromptProps {
|
|||||||
export function FolderPrompt({
|
export function FolderPrompt({
|
||||||
width,
|
width,
|
||||||
value,
|
value,
|
||||||
title = "download folder",
|
title = "default download folder",
|
||||||
|
subject,
|
||||||
|
submitLabel = "save",
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: FolderPromptProps) {
|
}: FolderPromptProps) {
|
||||||
@@ -24,7 +34,14 @@ export function FolderPrompt({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" width={width}>
|
<Box flexDirection="column" width={width}>
|
||||||
<Panel title={title} width={width} focused height={2}>
|
<Panel title={title} width={width} focused height={subject ? 3 : 2}>
|
||||||
|
{subject ? (
|
||||||
|
<Box>
|
||||||
|
<Text dimColor wrap="truncate-end">
|
||||||
|
{subject}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
<Box>
|
<Box>
|
||||||
<Text color={COLOR.accent}>{`${ICON.pointer} `}</Text>
|
<Text color={COLOR.accent}>{`${ICON.pointer} `}</Text>
|
||||||
<Box flexGrow={1} minWidth={0}>
|
<Box flexGrow={1} minWidth={0}>
|
||||||
@@ -37,11 +54,7 @@ export function FolderPrompt({
|
|||||||
</Box>
|
</Box>
|
||||||
</Panel>
|
</Panel>
|
||||||
<Box marginTop={1}>
|
<Box marginTop={1}>
|
||||||
<Text color={COLOR.alt}>↵</Text>
|
<PromptHints submitLabel={submitLabel} />
|
||||||
<Text dimColor> save</Text>
|
|
||||||
<Text dimColor>{` ${ICON.dot} `}</Text>
|
|
||||||
<Text color={COLOR.alt}>esc</Text>
|
|
||||||
<Text dimColor> cancel</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Box, Text } from "ink";
|
||||||
|
import { COLOR, ICON } from "../theme";
|
||||||
|
|
||||||
|
// The one-line action row under a modal prompt: the submit verb carries the
|
||||||
|
// visual weight, esc stays quiet.
|
||||||
|
export function PromptHints({ submitLabel }: { submitLabel: string }) {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Text color={COLOR.accent} bold>
|
||||||
|
↵
|
||||||
|
</Text>
|
||||||
|
<Text color={COLOR.text}>{` ${submitLabel}`}</Text>
|
||||||
|
<Text dimColor>{` ${ICON.dot} `}</Text>
|
||||||
|
<Text color={COLOR.alt}>esc</Text>
|
||||||
|
<Text dimColor> cancel</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -93,15 +93,15 @@ function Detail({ r, width }: { r: TorrentResult; width: number }) {
|
|||||||
</Box>
|
</Box>
|
||||||
<Box marginTop={1}>
|
<Box marginTop={1}>
|
||||||
<Text color={COLOR.accent} bold>
|
<Text color={COLOR.accent} bold>
|
||||||
d/D
|
d
|
||||||
</Text>
|
</Text>
|
||||||
<Text color={COLOR.text}> Download</Text>
|
<Text color={COLOR.text}> Download</Text>
|
||||||
<Text dimColor>{` ${ICON.dot} `}</Text>
|
<Text dimColor>{` ${ICON.dot} `}</Text>
|
||||||
<Text color={COLOR.accent} bold>
|
<Text color={COLOR.accent} bold>
|
||||||
y
|
y
|
||||||
</Text>
|
</Text>
|
||||||
<Text color={COLOR.text}> Copy magnet</Text>
|
<Text color={COLOR.text}> Copy</Text>
|
||||||
<Text dimColor>{` ${ICON.dot} `}</Text>
|
<Text dimColor>{` ${ICON.dot} `}</Text>
|
||||||
<Text color={COLOR.alt}>esc</Text>
|
<Text color={COLOR.alt}>esc</Text>
|
||||||
<Text dimColor> back</Text>
|
<Text dimColor> back</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Box, Text, useInput } from "ink";
|
import { Box, Text, useInput } from "ink";
|
||||||
import { TextField } from "./TextField";
|
import { TextField } from "./TextField";
|
||||||
import { Panel } from "./Panel";
|
import { Panel } from "./Panel";
|
||||||
|
import { PromptHints } from "./PromptHints";
|
||||||
import { formatTrackers, parseTrackers } from "../../config/trackers";
|
import { formatTrackers, parseTrackers } from "../../config/trackers";
|
||||||
import { COLOR, ICON } from "../theme";
|
import { COLOR, ICON } from "../theme";
|
||||||
|
|
||||||
@@ -18,7 +19,12 @@ export function TrackersPrompt({ width, value, onSubmit, onCancel }: TrackersPro
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" width={width}>
|
<Box flexDirection="column" width={width}>
|
||||||
<Panel title="extra trackers" width={width} focused height={2}>
|
<Panel title="extra trackers" width={width} focused height={3}>
|
||||||
|
<Box>
|
||||||
|
<Text dimColor wrap="truncate-end">
|
||||||
|
Comma or space separated. Empty clears. Applies to new adds.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Text color={COLOR.accent}>{`${ICON.pointer} `}</Text>
|
<Text color={COLOR.accent}>{`${ICON.pointer} `}</Text>
|
||||||
<Box flexGrow={1} minWidth={0}>
|
<Box flexGrow={1} minWidth={0}>
|
||||||
@@ -30,17 +36,8 @@ export function TrackersPrompt({ width, value, onSubmit, onCancel }: TrackersPro
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Panel>
|
</Panel>
|
||||||
<Box marginTop={1} flexDirection="column">
|
<Box marginTop={1}>
|
||||||
<Box>
|
<PromptHints submitLabel="save" />
|
||||||
<Text color={COLOR.alt}>↵</Text>
|
|
||||||
<Text dimColor> save</Text>
|
|
||||||
<Text dimColor>{` ${ICON.dot} `}</Text>
|
|
||||||
<Text color={COLOR.alt}>esc</Text>
|
|
||||||
<Text dimColor> cancel</Text>
|
|
||||||
</Box>
|
|
||||||
<Text dimColor>
|
|
||||||
Separate with commas or spaces. Empty saves an empty list. Applies to new adds.
|
|
||||||
</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,6 +29,16 @@ describe("filterResults", () => {
|
|||||||
expect(filterResults(list, true).map((x) => x.infoHash)).toEqual(["b"]);
|
expect(filterResults(list, true).map((x) => x.infoHash)).toEqual(["b"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps zero-seeder rows from sources that report no health data", () => {
|
||||||
|
const list = [
|
||||||
|
r({ infoHash: "a", seeders: 0, source: "fitgirl" }),
|
||||||
|
r({ infoHash: "b", seeders: 0 }),
|
||||||
|
r({ infoHash: "c", seeders: 0, source: "subsplease" }),
|
||||||
|
r({ infoHash: "d", seeders: 3 }),
|
||||||
|
];
|
||||||
|
expect(filterResults(list, true).map((x) => x.infoHash)).toEqual(["a", "c", "d"]);
|
||||||
|
});
|
||||||
|
|
||||||
it("does not mutate the input array", () => {
|
it("does not mutate the input array", () => {
|
||||||
const list = [r({ infoHash: "a", seeders: 0 }), r({ infoHash: "b", seeders: 2 })];
|
const list = [r({ infoHash: "a", seeders: 0 }), r({ infoHash: "b", seeders: 2 })];
|
||||||
const before = list.map((x) => x.infoHash);
|
const before = list.map((x) => x.infoHash);
|
||||||
|
|||||||
+4
-1
@@ -1,3 +1,4 @@
|
|||||||
|
import { getSource } from "../sources/registry";
|
||||||
import type { TorrentResult } from "../sources/types";
|
import type { TorrentResult } from "../sources/types";
|
||||||
|
|
||||||
export function filterResults(
|
export function filterResults(
|
||||||
@@ -5,5 +6,7 @@ export function filterResults(
|
|||||||
hideDead: boolean,
|
hideDead: boolean,
|
||||||
): TorrentResult[] {
|
): TorrentResult[] {
|
||||||
if (!hideDead) return list;
|
if (!hideDead) return list;
|
||||||
return list.filter((r) => r.seeders > 0);
|
// Sources without swarm data report seeders: 0 for everything (unknown, not
|
||||||
|
// dead), so the filter only judges rows whose source actually reports health.
|
||||||
|
return list.filter((r) => r.seeders > 0 || !getSource(r.source).reportsHealth);
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-36
@@ -18,7 +18,7 @@ export const HELP_GROUPS: HelpGroup[] = [
|
|||||||
{ keys: "↵", label: "Open" },
|
{ keys: "↵", label: "Open" },
|
||||||
{ keys: "tab", label: "Switch pane" },
|
{ keys: "tab", label: "Switch pane" },
|
||||||
{ keys: "esc", label: "Back" },
|
{ keys: "esc", label: "Back" },
|
||||||
{ keys: "o", label: "Download folder" },
|
{ keys: "o", label: "Default download folder" },
|
||||||
{ keys: "t", label: "Extra trackers" },
|
{ keys: "t", label: "Extra trackers" },
|
||||||
{ keys: "q", label: "Quit" },
|
{ keys: "q", label: "Quit" },
|
||||||
],
|
],
|
||||||
@@ -27,9 +27,7 @@ export const HELP_GROUPS: HelpGroup[] = [
|
|||||||
title: "Search",
|
title: "Search",
|
||||||
hints: [
|
hints: [
|
||||||
{ keys: "/", label: "Edit search" },
|
{ keys: "/", label: "Edit search" },
|
||||||
{ keys: "↵", label: "Run search" },
|
{ keys: "d/D", label: "Download (D picks folder)" },
|
||||||
{ keys: "d", label: "Download" },
|
|
||||||
{ keys: "D", label: "Download to a folder…" },
|
|
||||||
{ keys: "s", label: "Sort results" },
|
{ keys: "s", label: "Sort results" },
|
||||||
{ keys: "z", label: "Hide dead torrents" },
|
{ keys: "z", label: "Hide dead torrents" },
|
||||||
{ keys: "y", label: "Copy magnet" },
|
{ keys: "y", label: "Copy magnet" },
|
||||||
@@ -44,7 +42,7 @@ export const HELP_GROUPS: HelpGroup[] = [
|
|||||||
{ keys: "f", label: "Retry failed" },
|
{ keys: "f", label: "Retry failed" },
|
||||||
{ keys: "d", label: "Download again" },
|
{ keys: "d", label: "Download again" },
|
||||||
{ keys: "e", label: "Open folder" },
|
{ keys: "e", label: "Open folder" },
|
||||||
{ keys: "T", label: "Export .torrent" },
|
{ keys: "s", label: "Export torrent file" },
|
||||||
{ keys: "x", label: "Clear recent" },
|
{ keys: "x", label: "Clear recent" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -59,7 +57,8 @@ export const HELP_GROUPS: HelpGroup[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Footer labels stay terse so the contextual hint row never wraps; the `?`
|
// Footer labels stay terse so the contextual hint row never wraps; the `?`
|
||||||
// overlay (HELP_GROUPS) carries the full, descriptive list.
|
// overlay (HELP_GROUPS) carries the full, descriptive list. Rare or
|
||||||
|
// self-announcing actions (z) stay `?`-only to keep every row inside 80 cols.
|
||||||
const NAVIGATE: Hint = { keys: "↑↓←→", label: "Move" };
|
const NAVIGATE: Hint = { keys: "↑↓←→", label: "Move" };
|
||||||
|
|
||||||
const ALWAYS: Hint = { keys: "?", label: "Keys" };
|
const ALWAYS: Hint = { keys: "?", label: "Keys" };
|
||||||
@@ -68,7 +67,7 @@ const SWITCH: Hint = { keys: "tab", label: "Switch" };
|
|||||||
|
|
||||||
const FOLDER: Hint = { keys: "e", label: "Folder" };
|
const FOLDER: Hint = { keys: "e", label: "Folder" };
|
||||||
|
|
||||||
const TORRENT: Hint = { keys: "T", label: "Torrent" };
|
const TORRENT: Hint = { keys: "s", label: "Export" };
|
||||||
|
|
||||||
export function footerHints(
|
export function footerHints(
|
||||||
region: Region,
|
region: Region,
|
||||||
@@ -92,28 +91,13 @@ export function footerHints(
|
|||||||
}
|
}
|
||||||
if (section === "downloads") {
|
if (section === "downloads") {
|
||||||
if (downloadFocus === "paused") {
|
if (downloadFocus === "paused") {
|
||||||
return [
|
return [{ keys: "p", label: "Resume" }, { keys: "c", label: "Cancel" }, FOLDER, TORRENT, SWITCH, ALWAYS];
|
||||||
{ keys: "p", label: "Resume" },
|
|
||||||
{ keys: "c", label: "Cancel" },
|
|
||||||
FOLDER,
|
|
||||||
TORRENT,
|
|
||||||
SWITCH,
|
|
||||||
ALWAYS,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
if (downloadFocus === "failed") {
|
if (downloadFocus === "failed") {
|
||||||
return [
|
return [{ keys: "f", label: "Retry" }, { keys: "c", label: "Remove" }, FOLDER, TORRENT, SWITCH, ALWAYS];
|
||||||
{ keys: "f", label: "Retry" },
|
|
||||||
{ keys: "c", label: "Remove" },
|
|
||||||
FOLDER,
|
|
||||||
TORRENT,
|
|
||||||
SWITCH,
|
|
||||||
ALWAYS,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
if (downloadFocus === "recent") {
|
if (downloadFocus === "recent") {
|
||||||
return [
|
return [
|
||||||
NAVIGATE,
|
|
||||||
{ keys: "d", label: "Redownload" },
|
{ keys: "d", label: "Redownload" },
|
||||||
{ keys: "c", label: "Remove" },
|
{ keys: "c", label: "Remove" },
|
||||||
{ keys: "x", label: "Clear" },
|
{ keys: "x", label: "Clear" },
|
||||||
@@ -123,23 +107,15 @@ export function footerHints(
|
|||||||
ALWAYS,
|
ALWAYS,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return [
|
return [{ keys: "p", label: "Pause" }, { keys: "c", label: "Cancel" }, FOLDER, TORRENT, SWITCH, ALWAYS];
|
||||||
{ keys: "p", label: "Pause" },
|
|
||||||
{ keys: "c", label: "Cancel" },
|
|
||||||
FOLDER,
|
|
||||||
TORRENT,
|
|
||||||
SWITCH,
|
|
||||||
ALWAYS,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
NAVIGATE,
|
NAVIGATE,
|
||||||
// d downloads to the default folder, D asks where; the `?` sheet spells
|
// The footer advertises only the default download key; D (download to a
|
||||||
// out the difference, the footer just shows both keys exist.
|
// chosen folder) stays bound but lives in the `?` sheet alone.
|
||||||
{ keys: "d/D", label: "Download" },
|
{ keys: "d", label: "Download" },
|
||||||
{ keys: "y", label: "Copy" },
|
{ keys: "y", label: "Copy" },
|
||||||
{ keys: "s", label: "Sort" },
|
{ keys: "s", label: "Sort" },
|
||||||
{ keys: "z", label: "Alive" },
|
|
||||||
{ keys: "/", label: "Search" },
|
{ keys: "/", label: "Search" },
|
||||||
SWITCH,
|
SWITCH,
|
||||||
ALWAYS,
|
ALWAYS,
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ export interface Store {
|
|||||||
}) => void;
|
}) => void;
|
||||||
copyMagnet: (input: { name: string; magnet: string }) => void;
|
copyMagnet: (input: { name: string; magnet: string }) => void;
|
||||||
openDownloadFolder: (dir: string) => void;
|
openDownloadFolder: (dir: string) => void;
|
||||||
|
// Copies the cached .torrent metadata into the item's download folder and
|
||||||
|
// reports the outcome through the notice line.
|
||||||
|
exportTorrent: (input: { id: string; name: string }) => void;
|
||||||
|
|
||||||
notice: string | null;
|
notice: string | null;
|
||||||
setNotice: (s: string | null) => void;
|
setNotice: (s: string | null) => void;
|
||||||
|
|||||||
Reference in New Issue
Block a user