feat(desktop): improve torrent file integration

This commit is contained in:
Lionel DARNIS 2026-05-30 10:45:45 +02:00
parent 00b9748516
commit a1d4aab93f
12 changed files with 717 additions and 18 deletions

View file

@ -23,9 +23,30 @@ export interface TorrentFileAttributes {
export interface TorrentDetails {
name: string | null;
info_hash: string;
output_folder: string;
files: Array<TorrentFile>;
}
export interface LocalTorrentFile {
kind: "local-file";
path: string;
name: string;
}
export type TorrentInput = string | File | LocalTorrentFile;
export function localTorrentFile(path: string): LocalTorrentFile {
return {
kind: "local-file",
path,
name: path.split(/[\\/]/).pop() || path,
};
}
export function isLocalTorrentFile(data: TorrentInput): data is LocalTorrentFile {
return typeof data === "object" && !(data instanceof File) && data.kind === "local-file";
}
export interface AddTorrentResponse {
id: number | null;
details: TorrentDetails;
@ -192,9 +213,10 @@ export interface RqbitAPI {
filename?: string | null,
) => string | null;
uploadTorrent: (
data: string | File,
data: TorrentInput,
opts?: AddTorrentOptions,
) => Promise<AddTorrentResponse>;
openTorrentOutput?: (index: number) => Promise<void>;
pause: (index: number) => Promise<void>;
updateOnlyFiles: (index: number, files: number[]) => Promise<void>;

View file

@ -93,9 +93,82 @@ export const TorrentRow: React.FC<{
};
const [extendedView, setExtendedView] = useState(false);
const [contextMenu, setContextMenu] = useState<{
x: number;
y: number;
} | null>(null);
useEffect(() => {
if (!contextMenu) {
return;
}
const close = () => setContextMenu(null);
const closeOnEscape = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setContextMenu(null);
}
};
window.addEventListener("click", close);
window.addEventListener("keydown", closeOnEscape);
return () => {
window.removeEventListener("click", close);
window.removeEventListener("keydown", closeOnEscape);
};
}, [contextMenu]);
const openOutput = () => {
if (!API.openTorrentOutput) {
return;
}
API.openTorrentOutput(id).catch((e) => {
setCloseableError({
text: `Error opening torrent output id=${id}`,
details: e as ErrorDetails,
});
});
setContextMenu(null);
};
const handleContextMenu = (event: React.MouseEvent) => {
if (!API.openTorrentOutput) {
return;
}
event.preventDefault();
setContextMenu({ x: event.clientX, y: event.clientY });
};
return (
<div className="flex flex-col border p-2 border-gray-200 rounded-xl shadow-xs hover:drop-shadow-sm dark:bg-slate-800 dark:border-slate-900">
<div
className="flex flex-col border p-2 border-gray-200 rounded-xl shadow-xs hover:drop-shadow-sm dark:bg-slate-800 dark:border-slate-900"
onContextMenu={handleContextMenu}
>
{contextMenu && (
<div
className="fixed z-50 min-w-52 overflow-hidden rounded-md border border-gray-200 bg-white py-1 text-sm shadow-lg dark:border-slate-700 dark:bg-slate-800"
style={{ left: contextMenu.x, top: contextMenu.y }}
onClick={(event) => event.stopPropagation()}
>
<button
className="block w-full px-3 py-2 text-left text-gray-800 hover:bg-gray-100 dark:text-slate-100 dark:hover:bg-slate-700"
onClick={openOutput}
>
Open download folder
</button>
<button
className="block w-full px-3 py-2 text-left text-gray-800 hover:bg-gray-100 dark:text-slate-100 dark:hover:bg-slate-700"
onClick={() => {
setExtendedView(true);
setContextMenu(null);
}}
>
Show files
</button>
</div>
)}
<section className="flex flex-col lg:flex-row items-center gap-2">
{/* Icon */}
<div className="hidden md:block">{statusIcon("w-10 h-10")}</div>

View file

@ -9,6 +9,7 @@ import {
FaPlay,
FaTrash,
FaClipboardList,
FaFolderOpen,
} from "react-icons/fa";
import { useErrorStore } from "../../stores/errorStore";
import { ErrorComponent } from "../ErrorComponent";
@ -34,6 +35,22 @@ export const TorrentActions: React.FC<{
const API = useContext(APIContext);
const openOutput = () => {
if (!API.openTorrentOutput) {
return;
}
setDisabled(true);
API.openTorrentOutput(id)
.catch((e) => {
setCloseableError({
text: `Error opening torrent output id=${id}`,
details: e,
});
})
.finally(() => setDisabled(false));
};
const unpause = () => {
setDisabled(true);
API.start(id)
@ -136,6 +153,11 @@ export const TorrentActions: React.FC<{
<FaCog className="hover:text-green-600" />
</IconButton>
)}
{API.openTorrentOutput && (
<IconButton onClick={openOutput} disabled={disabled}>
<FaFolderOpen className="hover:text-blue-500" />
</IconButton>
)}
<IconButton onClick={startDeleting} disabled={disabled}>
<FaTrash className="hover:text-red-500" />
</IconButton>

View file

@ -2,6 +2,7 @@ import { ReactNode, useContext, useEffect, useState } from "react";
import {
AddTorrentResponse,
ErrorDetails as ApiErrorDetails,
TorrentInput,
} from "../../api-types";
import { APIContext } from "../../context";
import { ErrorWithLabel } from "../../rqbit-web";
@ -10,7 +11,7 @@ import { Button } from "./Button";
export const UploadButton: React.FC<{
onClick: () => void;
data: string | File | null;
data: TorrentInput | null;
resetData: () => void;
children: ReactNode;
className?: string;

View file

@ -1,5 +1,9 @@
import { useContext, useEffect, useState } from "react";
import { AddTorrentResponse, AddTorrentOptions } from "../../api-types";
import {
AddTorrentResponse,
AddTorrentOptions,
TorrentInput,
} from "../../api-types";
import { APIContext } from "../../context";
import { ErrorComponent } from "../ErrorComponent";
import { ErrorWithLabel } from "../../rqbit-web";
@ -19,7 +23,7 @@ export const FileSelectionModal = (props: {
listTorrentResponse: AddTorrentResponse | null;
listTorrentError: ErrorWithLabel | null;
listTorrentLoading: boolean;
data: string | File;
data: TorrentInput;
}) => {
let {
onHide,

View file

@ -6,6 +6,7 @@ import {
SessionStats,
TorrentDetails,
TorrentStats,
isLocalTorrentFile,
} from "./api-types";
// Define API URL and base path
@ -99,6 +100,12 @@ export const API: RqbitAPI & { getVersion: () => Promise<string> } = {
},
uploadTorrent: (data, opts): Promise<AddTorrentResponse> => {
if (isLocalTorrentFile(data)) {
return Promise.reject({
text: "Local torrent file paths are only supported in rqbit desktop.",
});
}
let url = "/torrents?&overwrite=true";
if (opts?.list_only) {
url += "&list_only=true";