feat: implement native OS file management and trash utilities

This commit is contained in:
NimBold
2026-06-16 15:45:22 +03:30
parent e41e761b33
commit 2727d07d62
7 changed files with 185 additions and 13 deletions
+84 -6
View File
@@ -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"
+1
View File
@@ -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"
+83
View File
@@ -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<String>) -> 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(())
}
+2
View File
@@ -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
])
+2 -2
View File
@@ -255,7 +255,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ 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<DownloadTableProps> = ({ 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);
}
+3
View File
@@ -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 };
+10 -5
View File
@@ -204,17 +204,22 @@ export const useDownloadStore = create<DownloadState>((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) => ({