feat(desktop): supprimer tous les torrents en une action

Ajoute api_torrents_delete_all sur l'Api librqbit : itere sur tous les
torrents de la session, les supprime avec ou sans les donnees selon
remove_files, collecte les erreurs sans interrompre la boucle et ne
renvoie une 500 qu'a la fin si au moins une suppression a echoue.

Cote desktop, la commande Tauri torrents_delete_all expose l'appel, et
la WebUI ajoute un bouton corbeille dans RootContent avec une modale de
confirmation DeleteAllTorrentsModal.

Limite connue : aucune route HTTP /torrents/delete_all n'est enregistree
dans http_api/handlers. Le chemin Tauri fonctionne (desktop/src/api.tsx
passe par invokeAPI), mais la WebUI servie en mode navigateur appellera
POST /torrents/delete_all et recevra un 404. La route reste a cabler
pour que le bouton fonctionne hors application desktop.

Ce correctif tournait depuis des semaines uniquement dans l'arbre de
travail, non versionne, alors qu'il etait deja compile dans les paquets
rqbit et rqbit-desktop installes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lionel DARNIS 2026-08-07 17:29:33 +02:00
parent 99219f90bc
commit 5d68aaf352
9 changed files with 233 additions and 0 deletions

View file

@ -202,6 +202,7 @@ export interface RqbitAPI {
start: (index: number) => Promise<void>;
forget: (index: number) => Promise<void>;
delete: (index: number) => Promise<void>;
deleteAll: (removeFiles?: boolean) => Promise<void>;
openOutputFolder?: (path: string) => Promise<void>;
stats: () => Promise<SessionStats>;
}

View file

@ -1,7 +1,11 @@
import { useState } from "react";
import { TorrentsList } from "./TorrentsList";
import { ErrorComponent } from "./ErrorComponent";
import { useTorrentStore } from "../stores/torrentStore";
import { useErrorStore } from "../stores/errorStore";
import { IconButton } from "./buttons/IconButton";
import { FiTrash2 } from "react-icons/fi";
import { DeleteAllTorrentsModal } from "./modal/DeleteAllTorrentsModal";
export const RootContent = (props: {}) => {
let closeableError = useErrorStore((state) => state.closeableError);
@ -11,6 +15,7 @@ export const RootContent = (props: {}) => {
let torrentsInitiallyLoading = useTorrentStore(
(state) => state.torrentsInitiallyLoading
);
const [deleteAllOpened, setDeleteAllOpened] = useState(false);
return (
<div className="container mx-auto">
@ -19,7 +24,18 @@ export const RootContent = (props: {}) => {
remove={() => setCloseableError(null)}
/>
<ErrorComponent error={otherError} />
{torrents && torrents.length > 0 && (
<div className="flex justify-end px-4 py-2">
<IconButton onClick={() => setDeleteAllOpened(true)}>
<FiTrash2 className="hover:text-red-500" />
</IconButton>
</div>
)}
<TorrentsList torrents={torrents} loading={torrentsInitiallyLoading} />
<DeleteAllTorrentsModal
show={deleteAllOpened}
onHide={() => setDeleteAllOpened(false)}
/>
</div>
);
};

View file

@ -0,0 +1,89 @@
import { useContext, useState } from "react";
import { APIContext } from "../../context";
import { ErrorWithLabel } from "../../rqbit-web";
import { ErrorComponent } from "../ErrorComponent";
import { Spinner } from "../Spinner";
import { Modal } from "./Modal";
import { ModalBody } from "./ModalBody";
import { ModalFooter } from "./ModalFooter";
import { Button } from "../buttons/Button";
import { useTorrentStore } from "../../stores/torrentStore";
export const DeleteAllTorrentsModal: React.FC<{
show: boolean;
onHide: () => void;
}> = ({ show, onHide }) => {
if (!show) {
return null;
}
const [deleteFiles, setDeleteFiles] = useState(false);
const [error, setError] = useState<ErrorWithLabel | null>(null);
const [deleting, setDeleting] = useState(false);
const API = useContext(APIContext);
const refreshTorrents = useTorrentStore((state) => state.refreshTorrents);
const close = () => {
setDeleteFiles(false);
setError(null);
setDeleting(false);
onHide();
};
const deleteAllTorrents = () => {
setDeleting(true);
API.deleteAll(deleteFiles)
.then(() => {
refreshTorrents();
close();
})
.catch((e) => {
setError({
text: "Error deleting all torrents",
details: e,
});
setDeleting(false);
});
};
return (
<Modal isOpen={show} onClose={onHide} title="Delete all torrents">
<ModalBody>
<p className="text-gray-700 dark:text-slate-300">
Are you sure you want to delete <strong>all</strong> torrents? This
action cannot be undone.
</p>
<div className="mt-4 flex items-center">
<input
type="checkbox"
id="deleteFilesAll"
className="form-checkbox h-4 w-4 text-blue-500"
onChange={() => setDeleteFiles(!deleteFiles)}
checked={deleteFiles}
placeholder="Also delete files"
/>
<label
htmlFor="deleteFilesAll"
className="ml-2 text-gray-700 dark:text-slate-300"
>
Also delete downloaded files
</label>
</div>
{error && <ErrorComponent error={error} />}
</ModalBody>
<ModalFooter>
{deleting && <Spinner />}
<Button variant="cancel" onClick={close}>
Cancel
</Button>
<Button variant="danger" onClick={deleteAllTorrents} disabled={deleting}>
Delete All
</Button>
</ModalFooter>
</Modal>
);
};

View file

@ -29,6 +29,9 @@ export const APIContext = createContext<RqbitAPI>({
delete: () => {
throw new Error("Function not implemented.");
},
deleteAll: () => {
throw new Error("Function not implemented.");
},
getTorrentStreamUrl: () => {
throw new Error("Function not implemented.");
},

View file

@ -151,6 +151,13 @@ export const API: RqbitAPI & { getVersion: () => Promise<string> } = {
delete: (index: number): Promise<void> => {
return makeRequest("POST", `/torrents/${index}/delete`);
},
deleteAll: (removeFiles: boolean = false): Promise<void> => {
let url = "/torrents/delete_all";
if (removeFiles) {
url += "?remove_files=true";
}
return makeRequest("POST", url);
},
getVersion: async (): Promise<string> => {
const r = await makeRequest("GET", "/");
return r.version;