From 5d68aaf35220fecde955a363b420a2691519ce05 Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Fri, 7 Aug 2026 17:29:33 +0200 Subject: [PATCH] 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) --- Cargo.lock | 60 +++++++++++++ crates/librqbit/src/api.rs | 42 +++++++++ crates/librqbit/webui/src/api-types.ts | 1 + .../webui/src/components/RootContent.tsx | 16 ++++ .../modal/DeleteAllTorrentsModal.tsx | 89 +++++++++++++++++++ crates/librqbit/webui/src/context.tsx | 3 + crates/librqbit/webui/src/http-api.ts | 7 ++ desktop/src-tauri/src/main.rs | 12 +++ desktop/src/api.tsx | 3 + 9 files changed, 233 insertions(+) create mode 100644 crates/librqbit/webui/src/components/modal/DeleteAllTorrentsModal.tsx diff --git a/Cargo.lock b/Cargo.lock index 226024a..65aa04d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7409,3 +7409,63 @@ dependencies = [ "quote", "syn 2.0.101", ] + +[[patch.unused]] +name = "dpi" +version = "0.1.2" + +[[patch.unused]] +name = "winit" +version = "0.31.0-beta.2" + +[[patch.unused]] +name = "winit-android" +version = "0.31.0-beta.2" + +[[patch.unused]] +name = "winit-appkit" +version = "0.31.0-beta.2" + +[[patch.unused]] +name = "winit-common" +version = "0.31.0-beta.2" + +[[patch.unused]] +name = "winit-core" +version = "0.31.0-beta.2" + +[[patch.unused]] +name = "winit-orbital" +version = "0.31.0-beta.2" + +[[patch.unused]] +name = "winit-uikit" +version = "0.31.0-beta.2" + +[[patch.unused]] +name = "winit-wayland" +version = "0.31.0-beta.2" + +[[patch.unused]] +name = "winit-web" +version = "0.31.0-beta.2" + +[[patch.unused]] +name = "winit-win32" +version = "0.31.0-beta.2" + +[[patch.unused]] +name = "winit-x11" +version = "0.31.0-beta.2" + +[[patch.unused]] +name = "softbuffer" +version = "0.4.1" + +[[patch.unused]] +name = "cosmic-freedesktop-icons" +version = "0.4.0" + +[[patch.unused]] +name = "smithay-clipboard" +version = "0.8.0" diff --git a/crates/librqbit/src/api.rs b/crates/librqbit/src/api.rs index cef6b72..1ef76d2 100644 --- a/crates/librqbit/src/api.rs +++ b/crates/librqbit/src/api.rs @@ -325,6 +325,48 @@ impl Api { Ok(Default::default()) } + /// Delete all torrents, optionally keeping the downloaded data. + pub async fn api_torrents_delete_all( + &self, + remove_files: bool, + ) -> Result { + let ids: Vec = { + let g = self.session.with_torrents(|torrents| { + torrents.map(|(id, _)| id).collect() + }); + g + }; + + if ids.is_empty() { + return Ok(Default::default()); + } + + // Collect errors but continue deleting all torrents + let mut errors: Vec = Vec::new(); + for id in ids { + if let Err(e) = self + .session + .delete(TorrentIdOrHash::Id(id), remove_files) + .await + { + warn!(id, error=?e, "error deleting torrent"); + errors.push(format!("Torrent {}: {:?}", id, e)); + } + } + + if !errors.is_empty() { + return Err(ApiError::new_from_anyhow( + StatusCode::INTERNAL_SERVER_ERROR, + anyhow::anyhow!( + "Failed to delete some torrents:\n{}", + errors.join("\n") + ), + )); + } + + Ok(Default::default()) + } + pub async fn api_torrent_action_update_only_files( &self, idx: TorrentIdOrHash, diff --git a/crates/librqbit/webui/src/api-types.ts b/crates/librqbit/webui/src/api-types.ts index 6580d3e..e7ee454 100644 --- a/crates/librqbit/webui/src/api-types.ts +++ b/crates/librqbit/webui/src/api-types.ts @@ -202,6 +202,7 @@ export interface RqbitAPI { start: (index: number) => Promise; forget: (index: number) => Promise; delete: (index: number) => Promise; + deleteAll: (removeFiles?: boolean) => Promise; openOutputFolder?: (path: string) => Promise; stats: () => Promise; } diff --git a/crates/librqbit/webui/src/components/RootContent.tsx b/crates/librqbit/webui/src/components/RootContent.tsx index f1387e4..f232916 100644 --- a/crates/librqbit/webui/src/components/RootContent.tsx +++ b/crates/librqbit/webui/src/components/RootContent.tsx @@ -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 (
@@ -19,7 +24,18 @@ export const RootContent = (props: {}) => { remove={() => setCloseableError(null)} /> + {torrents && torrents.length > 0 && ( +
+ setDeleteAllOpened(true)}> + + +
+ )} + setDeleteAllOpened(false)} + />
); }; diff --git a/crates/librqbit/webui/src/components/modal/DeleteAllTorrentsModal.tsx b/crates/librqbit/webui/src/components/modal/DeleteAllTorrentsModal.tsx new file mode 100644 index 0000000..824a352 --- /dev/null +++ b/crates/librqbit/webui/src/components/modal/DeleteAllTorrentsModal.tsx @@ -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(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 ( + + +

+ Are you sure you want to delete all torrents? This + action cannot be undone. +

+ +
+ setDeleteFiles(!deleteFiles)} + checked={deleteFiles} + placeholder="Also delete files" + /> + +
+ + {error && } +
+ + + {deleting && } + + + +
+ ); +}; diff --git a/crates/librqbit/webui/src/context.tsx b/crates/librqbit/webui/src/context.tsx index 7bf10b8..ab7646a 100644 --- a/crates/librqbit/webui/src/context.tsx +++ b/crates/librqbit/webui/src/context.tsx @@ -29,6 +29,9 @@ export const APIContext = createContext({ delete: () => { throw new Error("Function not implemented."); }, + deleteAll: () => { + throw new Error("Function not implemented."); + }, getTorrentStreamUrl: () => { throw new Error("Function not implemented."); }, diff --git a/crates/librqbit/webui/src/http-api.ts b/crates/librqbit/webui/src/http-api.ts index 2e00df3..9094c1d 100644 --- a/crates/librqbit/webui/src/http-api.ts +++ b/crates/librqbit/webui/src/http-api.ts @@ -151,6 +151,13 @@ export const API: RqbitAPI & { getVersion: () => Promise } = { delete: (index: number): Promise => { return makeRequest("POST", `/torrents/${index}/delete`); }, + deleteAll: (removeFiles: boolean = false): Promise => { + let url = "/torrents/delete_all"; + if (removeFiles) { + url += "?remove_files=true"; + } + return makeRequest("POST", url); + }, getVersion: async (): Promise => { const r = await makeRequest("GET", "/"); return r.version; diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs index 9488203..4380ebc 100644 --- a/desktop/src-tauri/src/main.rs +++ b/desktop/src-tauri/src/main.rs @@ -560,6 +560,17 @@ async fn torrent_action_delete( state.api()?.api_torrent_action_delete(id).await } +#[tauri::command] +async fn torrents_delete_all( + state: tauri::State<'_, State>, + remove_files: bool, +) -> Result { + state + .api()? + .api_torrents_delete_all(remove_files) + .await +} + #[tauri::command] async fn torrent_action_pause( state: tauri::State<'_, State>, @@ -692,6 +703,7 @@ async fn start() { torrent_stats, torrent_create_from_url, torrent_action_delete, + torrents_delete_all, torrent_action_pause, torrent_action_forget, torrent_action_start, diff --git a/desktop/src/api.tsx b/desktop/src/api.tsx index 7f1d97f..ae2a468 100644 --- a/desktop/src/api.tsx +++ b/desktop/src/api.tsx @@ -133,6 +133,9 @@ export const makeAPI = (configuration: RqbitDesktopConfig): RqbitAPI => { delete: function (id: number): Promise { return invokeAPI("torrent_action_delete", { id }); }, + deleteAll: function (removeFiles: boolean = false): Promise { + return invokeAPI("torrents_delete_all", { removeFiles }); + }, openOutputFolder: function (path: string): Promise { return invokeAPI("open_output_folder", { path }); },