feat(desktop): Yoda integration patches on v8.1.1
- macOS-style window controls via gtk-decoration-layout - torrent file preview and context menu improvements - single-instance relay for new torrents/magnet links
This commit is contained in:
parent
00b9748516
commit
99219f90bc
8 changed files with 405 additions and 13 deletions
|
|
@ -23,6 +23,7 @@ export interface TorrentFileAttributes {
|
|||
export interface TorrentDetails {
|
||||
name: string | null;
|
||||
info_hash: string;
|
||||
output_folder?: string;
|
||||
files: Array<TorrentFile>;
|
||||
}
|
||||
|
||||
|
|
@ -201,5 +202,6 @@ export interface RqbitAPI {
|
|||
start: (index: number) => Promise<void>;
|
||||
forget: (index: number) => Promise<void>;
|
||||
delete: (index: number) => Promise<void>;
|
||||
openOutputFolder?: (path: string) => Promise<void>;
|
||||
stats: () => Promise<SessionStats>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useContext, useEffect, useState } from "react";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
TorrentDetails,
|
||||
TorrentId,
|
||||
|
|
@ -9,6 +9,7 @@ import {
|
|||
import { APIContext, RefreshTorrentStatsContext } from "../context";
|
||||
import { customSetInterval } from "../helper/customSetInterval";
|
||||
import { loopUntilSuccess } from "../helper/loopUntilSuccess";
|
||||
import { useTorrentStore } from "../stores/torrentStore";
|
||||
import { TorrentRow } from "./TorrentRow";
|
||||
|
||||
export const Torrent: React.FC<{
|
||||
|
|
@ -21,7 +22,13 @@ export const Torrent: React.FC<{
|
|||
null,
|
||||
);
|
||||
const [forceStatsRefresh, setForceStatsRefresh] = useState(0);
|
||||
const rowRef = useRef<HTMLDivElement>(null);
|
||||
const API = useContext(APIContext);
|
||||
const focusedTorrentId = useTorrentStore((state) => state.focusedTorrentId);
|
||||
const clearFocusedTorrent = useTorrentStore(
|
||||
(state) => state.clearFocusedTorrent,
|
||||
);
|
||||
const isFocusedTorrent = focusedTorrentId === id;
|
||||
|
||||
const forceStatsRefreshCallback = () => {
|
||||
setForceStatsRefresh(forceStatsRefresh + 1);
|
||||
|
|
@ -65,15 +72,38 @@ export const Torrent: React.FC<{
|
|||
[forceStatsRefresh],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFocusedTorrent) {
|
||||
return;
|
||||
}
|
||||
|
||||
rowRef.current?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
|
||||
const timeout = setTimeout(() => clearFocusedTorrent(id), 3000);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [clearFocusedTorrent, id, isFocusedTorrent]);
|
||||
|
||||
return (
|
||||
<RefreshTorrentStatsContext.Provider
|
||||
value={{ refresh: forceStatsRefreshCallback }}
|
||||
>
|
||||
<TorrentRow
|
||||
id={id}
|
||||
detailsResponse={detailsResponse}
|
||||
statsResponse={statsResponse}
|
||||
/>
|
||||
<div
|
||||
ref={rowRef}
|
||||
className={
|
||||
isFocusedTorrent
|
||||
? "rounded-xl ring-2 ring-blue-500 ring-offset-2 ring-offset-white dark:ring-offset-gray-900"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<TorrentRow
|
||||
id={id}
|
||||
detailsResponse={detailsResponse}
|
||||
statsResponse={statsResponse}
|
||||
/>
|
||||
</div>
|
||||
</RefreshTorrentStatsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@ export const TorrentRow: React.FC<{
|
|||
<TorrentActions
|
||||
id={id}
|
||||
statsResponse={statsResponse}
|
||||
outputFolder={detailsResponse?.output_folder}
|
||||
extendedView={extendedView}
|
||||
setExtendedView={setExtendedView}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { IconButton } from "./IconButton";
|
|||
import { DeleteTorrentModal } from "../modal/DeleteTorrentModal";
|
||||
import {
|
||||
FaCog,
|
||||
FaFolderOpen,
|
||||
FaPause,
|
||||
FaPlay,
|
||||
FaTrash,
|
||||
|
|
@ -16,24 +17,26 @@ import { ErrorComponent } from "../ErrorComponent";
|
|||
export const TorrentActions: React.FC<{
|
||||
id: number;
|
||||
statsResponse: TorrentStats;
|
||||
outputFolder?: string;
|
||||
extendedView: boolean;
|
||||
setExtendedView: (extendedView: boolean) => void;
|
||||
}> = ({ id, statsResponse, extendedView, setExtendedView }) => {
|
||||
}> = ({ id, statsResponse, outputFolder, extendedView, setExtendedView }) => {
|
||||
let state = statsResponse.state;
|
||||
|
||||
let [disabled, setDisabled] = useState<boolean>(false);
|
||||
let [deleting, setDeleting] = useState<boolean>(false);
|
||||
|
||||
let refreshCtx = useContext(RefreshTorrentStatsContext);
|
||||
const API = useContext(APIContext);
|
||||
|
||||
const canPause = state == "live";
|
||||
const canUnpause = state == "paused" || state == "error";
|
||||
const canConfigure = state == "paused" || state == "live";
|
||||
const canOpenOutputFolder =
|
||||
statsResponse.finished && !!outputFolder && !!API.openOutputFolder;
|
||||
|
||||
const setCloseableError = useErrorStore((state) => state.setCloseableError);
|
||||
|
||||
const API = useContext(APIContext);
|
||||
|
||||
const unpause = () => {
|
||||
setDisabled(true);
|
||||
API.start(id)
|
||||
|
|
@ -116,6 +119,19 @@ export const TorrentActions: React.FC<{
|
|||
});
|
||||
};
|
||||
|
||||
const openOutputFolder = () => {
|
||||
if (!API.openOutputFolder || !outputFolder) {
|
||||
return;
|
||||
}
|
||||
|
||||
API.openOutputFolder(outputFolder).catch((e) => {
|
||||
setCloseableError({
|
||||
text: `Error opening output folder for torrent id=${id}`,
|
||||
details: e,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full justify-center gap-2 dark:text-slate-300">
|
||||
{canUnpause && (
|
||||
|
|
@ -136,6 +152,11 @@ export const TorrentActions: React.FC<{
|
|||
<FaCog className="hover:text-green-600" />
|
||||
</IconButton>
|
||||
)}
|
||||
{canOpenOutputFolder && (
|
||||
<IconButton onClick={openOutputFolder} disabled={disabled}>
|
||||
<FaFolderOpen className="hover:text-green-600" />
|
||||
</IconButton>
|
||||
)}
|
||||
<IconButton onClick={startDeleting} disabled={disabled}>
|
||||
<FaTrash className="hover:text-red-500" />
|
||||
</IconButton>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ export interface TorrentStore {
|
|||
|
||||
refreshTorrents: () => void;
|
||||
setRefreshTorrents: (callback: () => void) => void;
|
||||
|
||||
focusedTorrentId: number | null;
|
||||
focusTorrent: (id: number) => void;
|
||||
clearFocusedTorrent: (id: number) => void;
|
||||
}
|
||||
|
||||
const torrentIdEquals = (t1: TorrentId, t2: TorrentId): boolean => {
|
||||
|
|
@ -51,4 +55,13 @@ export const useTorrentStore = create<TorrentStore>((set) => ({
|
|||
}),
|
||||
refreshTorrents: () => {},
|
||||
setRefreshTorrents: (callback) => set({ refreshTorrents: callback }),
|
||||
focusedTorrentId: null,
|
||||
focusTorrent: (id) => set({ focusedTorrentId: id }),
|
||||
clearFocusedTorrent: (id) =>
|
||||
set((prev) => {
|
||||
if (prev.focusedTorrentId !== id) {
|
||||
return {};
|
||||
}
|
||||
return { focusedTorrentId: null };
|
||||
}),
|
||||
}));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue