feat(desktop): improve torrent file integration
This commit is contained in:
parent
00b9748516
commit
a1d4aab93f
12 changed files with 717 additions and 18 deletions
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue