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 }); },