From 2727d07d62be0bfa97ab0b57dfb62991779a7250 Mon Sep 17 00:00:00 2001 From: NimBold Date: Tue, 16 Jun 2026 15:45:22 +0330 Subject: [PATCH] feat: implement native OS file management and trash utilities --- src-tauri/Cargo.lock | 90 +++++++++++++++++++++++++++++--- src-tauri/Cargo.toml | 1 + src-tauri/src/commands.rs | 83 +++++++++++++++++++++++++++++ src-tauri/src/lib.rs | 2 + src/components/DownloadTable.tsx | 4 +- src/ipc.ts | 3 ++ src/store/useDownloadStore.ts | 15 ++++-- 7 files changed, 185 insertions(+), 13 deletions(-) create mode 100644 src-tauri/src/commands.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 51bd4f3..cd6048a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1382,6 +1382,7 @@ dependencies = [ "tokio", "tokio-tungstenite", "tower-http", + "trash", "ts-rs", "url", "uuid", @@ -5479,6 +5480,24 @@ dependencies = [ "once_cell", ] +[[package]] +name = "trash" +version = "5.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7602e0c7d66ec2d92a8c917219fbc7894039efa2063b9064260110828a356f46" +dependencies = [ + "chrono", + "libc", + "log 0.4.32", + "objc2", + "objc2-foundation", + "once_cell", + "percent-encoding", + "scopeguard", + "urlencoding", + "windows 0.56.0", +] + [[package]] name = "tray-icon" version = "0.23.1" @@ -5653,6 +5672,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "urlpattern" version = "0.3.0" @@ -5979,8 +6004,8 @@ dependencies = [ "webview2-com-sys", "windows 0.61.3", "windows-core 0.61.2", - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", ] [[package]] @@ -6051,6 +6076,16 @@ dependencies = [ "windows-version", ] +[[package]] +name = "windows" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" +dependencies = [ + "windows-core 0.56.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.61.3" @@ -6094,14 +6129,26 @@ dependencies = [ "windows-core 0.62.2", ] +[[package]] +name = "windows-core" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" +dependencies = [ + "windows-implement 0.56.0", + "windows-interface 0.56.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", @@ -6113,8 +6160,8 @@ version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link 0.2.1", "windows-result 0.4.1", "windows-strings 0.5.1", @@ -6142,6 +6189,17 @@ dependencies = [ "windows-threading 0.2.1", ] +[[package]] +name = "windows-implement" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "windows-implement" version = "0.60.2" @@ -6153,6 +6211,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "windows-interface" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "windows-interface" version = "0.59.3" @@ -6218,6 +6287,15 @@ dependencies = [ "windows-strings 0.5.1", ] +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.3.4" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2da28a2..014aad4 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -56,6 +56,7 @@ url = "2" tauri-plugin-store = "2.4.3" log = "0.4.32" tauri-plugin-log = "2" +trash = "5" [target.'cfg(target_os = "macos")'.dependencies] cocoa = "0.25" objc = "0.2.7" diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs new file mode 100644 index 0000000..a48f577 --- /dev/null +++ b/src-tauri/src/commands.rs @@ -0,0 +1,83 @@ +use std::process::Command; +use std::path::Path; + +#[tauri::command] +pub async fn reveal_in_file_manager(path: String) -> Result<(), String> { + #[cfg(target_os = "macos")] + { + Command::new("open") + .arg("-R") + .arg(&path) + .spawn() + .map_err(|e| format!("Failed to reveal file: {}", e))?; + } + + #[cfg(target_os = "windows")] + { + Command::new("explorer.exe") + .arg(format!("/select,\"{}\"", path)) + .spawn() + .map_err(|e| format!("Failed to reveal file: {}", e))?; + } + + #[cfg(not(any(target_os = "macos", target_os = "windows")))] + { + let parent = Path::new(&path).parent().unwrap_or(Path::new("")); + Command::new("xdg-open") + .arg(parent) + .spawn() + .map_err(|e| format!("Failed to reveal file: {}", e))?; + } + + Ok(()) +} + +#[tauri::command] +pub async fn open_downloaded_file(path: String) -> Result<(), String> { + #[cfg(target_os = "macos")] + { + Command::new("open") + .arg(&path) + .spawn() + .map_err(|e| format!("Failed to open file: {}", e))?; + } + + #[cfg(target_os = "windows")] + { + Command::new("cmd") + .args(["/c", "start", "", &path]) + .spawn() + .map_err(|e| format!("Failed to open file: {}", e))?; + } + + #[cfg(not(any(target_os = "macos", target_os = "windows")))] + { + Command::new("xdg-open") + .arg(&path) + .spawn() + .map_err(|e| format!("Failed to open file: {}", e))?; + } + + Ok(()) +} + +#[tauri::command] +pub async fn trash_download_assets(path: String, partial_paths: Vec) -> Result<(), String> { + let p = Path::new(&path); + if p.exists() { + if let Err(e) = trash::delete(p) { + return Err(format!("Failed to trash primary file: {}", e)); + } + } + + for partial in partial_paths { + let p_partial = Path::new(&partial); + if p_partial.exists() { + if let Err(e) = trash::delete(p_partial) { + return Err(format!("Failed to trash partial file: {}", e)); + } + } + } + + Ok(()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 113620c..5af4c74 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -423,6 +423,7 @@ pub mod download; mod ipc; mod parity; pub mod error; +pub mod commands; pub use error::AppError; // Retained only for compatibility with the optional aria2 diagnostic monitor. @@ -1704,6 +1705,7 @@ pub fn run() { set_keychain_password, get_keychain_password, delete_keychain_password, check_file_exists, delete_file, toggle_tray_icon, set_extension_pairing_token, set_extension_frontend_ready, set_concurrent_limit, set_global_speed_limit, remove_download, + commands::reveal_in_file_manager, commands::open_downloaded_file, commands::trash_download_assets, parity::get_system_proxy, parity::get_file_category, parity::check_for_updates, parity::is_supported_media, parity::get_supported_media_domains, parity::create_category_directories ]) diff --git a/src/components/DownloadTable.tsx b/src/components/DownloadTable.tsx index 5fa1a38..0c7672f 100644 --- a/src/components/DownloadTable.tsx +++ b/src/components/DownloadTable.tsx @@ -255,7 +255,7 @@ export const DownloadTable: React.FC = ({ filter }) => { setContextMenu(null); try { const fullPath = await resolvePath(contextItem.destination || '~/Downloads', contextItem.fileName); - await invoke('open_file', { path: fullPath }); + await invoke('open_downloaded_file', { path: fullPath }); } catch (e) { console.error("Failed to open file:", e); } @@ -271,7 +271,7 @@ export const DownloadTable: React.FC = ({ filter }) => { setContextMenu(null); try { const fullPath = await resolvePath(contextItem.destination || '~/Downloads', contextItem.fileName); - await invoke('show_in_folder', { path: fullPath }); + await invoke('reveal_in_file_manager', { path: fullPath }); } catch (e) { console.error("Failed to show in folder:", e); } diff --git a/src/ipc.ts b/src/ipc.ts index f2d94fd..5da7036 100644 --- a/src/ipc.ts +++ b/src/ipc.ts @@ -60,6 +60,9 @@ type CommandMap = { test_deno: { args: undefined; result: string }; open_file: { args: { path: string }; result: void }; show_in_folder: { args: { path: string }; result: void }; + reveal_in_file_manager: { args: { path: string }; result: void }; + open_downloaded_file: { args: { path: string }; result: void }; + trash_download_assets: { args: { path: string; partialPaths: string[] }; result: void }; start_download: { args: StartDownloadArgs; result: void }; start_media_download: { args: StartMediaDownloadArgs; result: void }; pause_download: { args: { id: string }; result: void }; diff --git a/src/store/useDownloadStore.ts b/src/store/useDownloadStore.ts index 13fb23e..3831ef3 100644 --- a/src/store/useDownloadStore.ts +++ b/src/store/useDownloadStore.ts @@ -204,17 +204,22 @@ export const useDownloadStore = create((set, get) => ({ const item = get().downloads.find(d => d.id === id); if (item && item.status === 'downloading') { try { - const filepath = item.destination ? `${item.destination}/${item.fileName}` : null; - await invoke('remove_download', { id, filepath }); + // Just cancel the active download via the backend, don't delete files via remove_download + await invoke('remove_download', { id, filepath: null }); } catch (e) { console.error("Failed to terminate download on deletion:", e); } - } else if (item && deleteFile) { + } + + if (item && deleteFile) { try { const filepath = item.destination ? `${item.destination}/${item.fileName}` : null; - await invoke('remove_download', { id, filepath }); + if (filepath) { + const partialPaths = [`${filepath}.aria2`, `${filepath}.part`]; + await invoke('trash_download_assets', { path: filepath, partialPaths }); + } } catch (e) { - console.error("Failed to delete file from disk:", e); + console.error("Failed to trash file from disk:", e); } } set((state) => ({