librqbit 9.0.1

The main library used by rqbit torrent client. The binary is just a small wrapper on top of it.
Documentation
import { useContext, useEffect, useState } from "react";
import { APIContext } from "../../context";
import { loopUntilSuccess } from "../../helper/loopUntilSuccess";
import { useUIStore } from "../../stores/uiStore";
import { useTorrentStore } from "../../stores/torrentStore";
import { OverviewTab } from "./OverviewTab";
import { FilesTab } from "./FilesTab";
import { PeersTab } from "./PeersTab";
import { TabButton, TabList } from "../Tabs";

type TabId = "overview" | "files" | "peers";

export const DetailPane: React.FC = () => {
  const selectedTorrentIds = useUIStore((state) => state.selectedTorrentIds);
  const [activeTab, setActiveTab] = useState<TabId>("overview");

  const selectedArray = Array.from(selectedTorrentIds);
  const selectedCount = selectedArray.length;

  if (selectedCount === 0) {
    return (
      <div className="h-full border-t border-divider bg-surface-raised flex items-center justify-center">
        <p className="text-tertiary">Select a torrent to view details</p>
      </div>
    );
  }

  if (selectedCount > 1) {
    return (
      <div className="h-full border-t border-divider bg-surface-raised flex items-center justify-center">
        <p className="text-tertiary">{selectedCount} torrents selected</p>
      </div>
    );
  }

  const selectedId = selectedArray[0];

  return (
    <div className="h-full border-t border-divider flex flex-col bg-surface">
      <TabList className="bg-surface-raised">
        <TabButton
          id="overview"
          label="Overview"
          active={activeTab === "overview"}
          onClick={() => setActiveTab("overview")}
        />
        <TabButton
          id="files"
          label="Files"
          active={activeTab === "files"}
          onClick={() => setActiveTab("files")}
        />
        <TabButton
          id="peers"
          label="Peers"
          active={activeTab === "peers"}
          onClick={() => setActiveTab("peers")}
        />
      </TabList>
      <div className="flex-1 overflow-auto">
        <DetailPaneContent torrentId={selectedId} activeTab={activeTab} />
      </div>
    </div>
  );
};

interface DetailPaneContentProps {
  torrentId: number;
  activeTab: TabId;
}

const DetailPaneContent: React.FC<DetailPaneContentProps> = ({
  torrentId,
  activeTab,
}) => {
  const API = useContext(APIContext);
  const [fetchDetails, setFetchDetails] = useState(false);

  // Get torrent and details from store
  const torrent = useTorrentStore((state) =>
    state.torrents?.find((t) => t.id === torrentId),
  );
  const cachedDetails = useTorrentStore((state) => state.getDetails(torrentId));
  const setDetails = useTorrentStore((state) => state.setDetails);
  const refreshTorrents = useTorrentStore((state) => state.refreshTorrents);

  // Trigger fetch when Files tab is active and not cached
  useEffect(() => {
    if (activeTab !== "files") return;
    if (cachedDetails) return;
    setFetchDetails(true);
  }, [activeTab, torrentId, cachedDetails]);

  // Fetch details when requested
  useEffect(() => {
    if (!fetchDetails) return;
    return loopUntilSuccess(async () => {
      const details = await API.getTorrentDetails(torrentId);
      setDetails(torrentId, details);
      setFetchDetails(false);
    }, 1000);
  }, [fetchDetails, torrentId]);

  const forceRefreshCallback = () => {
    refreshTorrents();
    setFetchDetails(true);
  };

  const statsResponse = torrent?.stats ?? null;

  return (
    <>
      {activeTab === "overview" && <OverviewTab torrent={torrent ?? null} />}
      {activeTab === "files" && (
        <FilesTab
          torrentId={torrentId}
          detailsResponse={cachedDetails}
          statsResponse={statsResponse}
          onRefresh={forceRefreshCallback}
        />
      )}
      {activeTab === "peers" && <PeersTab torrent={torrent ?? null} />}
    </>
  );
};