diff --git a/package.json b/package.json index 0587462..cd58f1f 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@tauri-apps/api": "^2", "@tauri-apps/plugin-clipboard-manager": "^2.2.0", "@tauri-apps/plugin-opener": "^2", + "@tauri-apps/plugin-window-state": "~2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "embla-carousel-autoplay": "^8.5.2", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index e4b1099..9010741 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3753,6 +3753,7 @@ dependencies = [ "tauri-build", "tauri-plugin-clipboard-manager", "tauri-plugin-opener", + "tauri-plugin-window-state", "tokio", "tokio-macros", "tokio-util", @@ -4123,6 +4124,21 @@ dependencies = [ "zbus", ] +[[package]] +name = "tauri-plugin-window-state" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e344b512b0d99d9d06225f235d87d6c66d89496a3bf323d9b578d940596e6c" +dependencies = [ + "bitflags 2.8.0", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.11", +] + [[package]] name = "tauri-runtime" version = "2.3.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f55f4cb..eeaf6e7 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -32,3 +32,6 @@ pinyin = "0.10.0" tauri-plugin-clipboard-manager = "2.2.0" dirs = "6.0.0" base64 = "0.22.1" + +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] +tauri-plugin-window-state = "2" diff --git a/src-tauri/capabilities/desktop.json b/src-tauri/capabilities/desktop.json new file mode 100644 index 0000000..9be5102 --- /dev/null +++ b/src-tauri/capabilities/desktop.json @@ -0,0 +1,14 @@ +{ + "identifier": "desktop-capability", + "platforms": [ + "macOS", + "windows", + "linux" + ], + "windows": [ + "main" + ], + "permissions": [ + "window-state:default" + ] +} \ No newline at end of file diff --git a/src-tauri/src/handlers/app.rs b/src-tauri/src/handlers/app.rs index 2862ab9..cb161d9 100644 --- a/src-tauri/src/handlers/app.rs +++ b/src-tauri/src/handlers/app.rs @@ -1,6 +1,10 @@ -use crate::{handlers::server::get_json_server_url, models::app::{AppDetail, AppItem}, utils::UA}; -use crate::utils::format::format_icon_url; use super::category::get_all_apps; +use crate::utils::format::format_icon_url; +use crate::{ + handlers::server::get_json_server_url, + models::app::{AppDetail, AppItem}, + utils::UA, +}; #[tauri::command] pub async fn get_app_info(category: String, pkgname: String) -> Result { @@ -10,7 +14,7 @@ pub async fn get_app_info(category: String, pkgname: String) -> Result Result Result { let json_server_url = get_json_server_url(); - let url = format!("{}{}/{}/download-times.txt", json_server_url, category, pkgname); - + let url = format!( + "{}{}/{}/download-times.txt", + json_server_url, category, pkgname + ); + let client = reqwest::Client::new(); let response = client .get(&url) @@ -47,7 +56,7 @@ pub async fn get_download_times(category: String, pkgname: String) -> Result Result() .map_err(|e| e.to_string())?; - + Ok(times) } @@ -63,4 +72,4 @@ pub async fn get_download_times(category: String, pkgname: String) -> Result Result, String> { let all_apps = get_all_apps().await?; Ok(crate::utils::search::search_apps(&all_apps, &query)) -} \ No newline at end of file +} diff --git a/src-tauri/src/handlers/category.rs b/src-tauri/src/handlers/category.rs index 3127bfb..e3de028 100644 --- a/src-tauri/src/handlers/category.rs +++ b/src-tauri/src/handlers/category.rs @@ -1,12 +1,12 @@ +use super::server::get_json_server_url; +use crate::models::app::AppItem; use crate::models::category::Category; use crate::utils::format::format_icon_url; -use crate::models::app::AppItem; use crate::utils::UA; -use super::server::get_json_server_url; +use futures::future::join_all; +use lazy_static::lazy_static; use std::collections::HashMap; use std::sync::Mutex; -use lazy_static::lazy_static; -use futures::future::join_all; lazy_static! { static ref APPS_CACHE: Mutex>> = Mutex::new(HashMap::new()); @@ -23,7 +23,7 @@ pub async fn get_category_apps(category_id: String) -> Result, Stri // 如果缓存中没有,从服务器获取数据 let json_server_url = get_json_server_url(); let url = format!("{}{}/applist.json", json_server_url, category_id); - + let client = reqwest::Client::new(); let response = client .get(&url) @@ -31,12 +31,9 @@ pub async fn get_category_apps(category_id: String) -> Result, Stri .send() .await .map_err(|e| e.to_string())?; - - let mut apps: Vec = response - .json() - .await - .map_err(|e| e.to_string())?; - + + let mut apps: Vec = response.json().await.map_err(|e| e.to_string())?; + // 为每个应用设置图标URL for app in &mut apps { app.icon = Some(format_icon_url(&category_id, &app.pkgname)); @@ -44,8 +41,11 @@ pub async fn get_category_apps(category_id: String) -> Result, Stri } // 更新缓存 - APPS_CACHE.lock().unwrap().insert(category_id.clone(), apps.clone()); - + APPS_CACHE + .lock() + .unwrap() + .insert(category_id.clone(), apps.clone()); + Ok(apps) } @@ -56,17 +56,17 @@ pub async fn get_all_apps() -> Result, String> { .iter() .map(|category| get_category_apps(category.id.clone())) .collect(); - + let results = join_all(futures).await; let mut all_apps = Vec::new(); - + for result in results { match result { Ok(apps) => all_apps.extend(apps), - Err(e) => eprintln!("Error fetching apps for category: {}", e) + Err(e) => eprintln!("Error fetching apps for category: {}", e), } } - + Ok(all_apps) } @@ -80,7 +80,7 @@ pub async fn get_all_categories() -> Result, String> { // 如果缓存中没有,从服务器获取数据 let json_server_url = get_json_server_url(); let url = format!("{}category.json", json_server_url); - + let client = reqwest::Client::new(); let response = client .get(&url) @@ -88,14 +88,11 @@ pub async fn get_all_categories() -> Result, String> { .send() .await .map_err(|e| e.to_string())?; - - let categories: Vec = response - .json() - .await - .map_err(|e| e.to_string())?; + + let categories: Vec = response.json().await.map_err(|e| e.to_string())?; // 更新缓存 *CATEGORIES_CACHE.lock().unwrap() = Some(categories.clone()); - + Ok(categories) -} \ No newline at end of file +} diff --git a/src-tauri/src/handlers/deb.rs b/src-tauri/src/handlers/deb.rs index 650206a..8aa5ff6 100644 --- a/src-tauri/src/handlers/deb.rs +++ b/src-tauri/src/handlers/deb.rs @@ -18,7 +18,7 @@ pub async fn check_launch_app(pkgname: String) -> Result { .output() .map_err(|e| format!("启动应用失败: {}", e))?; - Ok(output.status.success()) + Ok(output.status.success()) } #[tauri::command] @@ -30,4 +30,4 @@ pub async fn launch_launch_app(pkgname: String) -> Result<(), String> { .map_err(|e| format!("启动应用失败: {}", e))?; Ok(()) -} \ No newline at end of file +} diff --git a/src-tauri/src/handlers/download.rs b/src-tauri/src/handlers/download.rs index aa86007..314f1ed 100644 --- a/src-tauri/src/handlers/download.rs +++ b/src-tauri/src/handlers/download.rs @@ -3,26 +3,48 @@ use tauri::State; use crate::{models::download::DownloadTaskResponse, utils::download_manager::DownloadManager}; #[tauri::command] -pub async fn get_downloads(manager: State<'_, DownloadManager>) -> Result, String> { +pub async fn get_downloads( + manager: State<'_, DownloadManager>, +) -> Result, String> { manager.get_downloads().await } #[tauri::command] -pub async fn add_download(category: String, pkgname: String, filename: String, name: String, manager: State<'_, DownloadManager>) -> Result<(), String> { - manager.add_download(category, pkgname, filename, name).await +pub async fn add_download( + category: String, + pkgname: String, + filename: String, + name: String, + manager: State<'_, DownloadManager>, +) -> Result<(), String> { + manager + .add_download(category, pkgname, filename, name) + .await } #[tauri::command] -pub async fn pause_download(category: String, pkgname: String, manager: State<'_, DownloadManager>) -> Result<(), String> { +pub async fn pause_download( + category: String, + pkgname: String, + manager: State<'_, DownloadManager>, +) -> Result<(), String> { manager.pause_download(category, pkgname).await } #[tauri::command] -pub async fn resume_download(category: String, pkgname: String, manager: State<'_, DownloadManager>) -> Result<(), String> { +pub async fn resume_download( + category: String, + pkgname: String, + manager: State<'_, DownloadManager>, +) -> Result<(), String> { manager.resume_download(category, pkgname).await } #[tauri::command] -pub async fn cancel_download(category: String, pkgname: String, manager: State<'_, DownloadManager>) -> Result<(), String> { +pub async fn cancel_download( + category: String, + pkgname: String, + manager: State<'_, DownloadManager>, +) -> Result<(), String> { manager.cancel_download(category, pkgname).await -} \ No newline at end of file +} diff --git a/src-tauri/src/handlers/file.rs b/src-tauri/src/handlers/file.rs index de99391..2414a77 100644 --- a/src-tauri/src/handlers/file.rs +++ b/src-tauri/src/handlers/file.rs @@ -4,10 +4,10 @@ use std::fs; pub fn save_text_file(filename: String, content: String) -> Result<(), String> { let config_dir = dirs::config_dir().ok_or("无法获取配置目录")?; let dir = config_dir.join(env!("CARGO_PKG_NAME")); - + // 确保目录存在 fs::create_dir_all(&dir).map_err(|e| format!("创建目录失败: {}", e))?; - + let file_path = dir.join(filename); fs::write(file_path, content).map_err(|e| format!("写入文件失败: {}", e)) } @@ -17,6 +17,6 @@ pub fn read_text_file(filename: String) -> Result { let config_dir = dirs::config_dir().ok_or("无法获取配置目录")?; let dir = config_dir.join(env!("CARGO_PKG_NAME")); let file_path = dir.join(filename); - + fs::read_to_string(file_path).map_err(|e| format!("读取文件失败: {}", e)) -} \ No newline at end of file +} diff --git a/src-tauri/src/handlers/home.rs b/src-tauri/src/handlers/home.rs index 272b579..d7ccb68 100644 --- a/src-tauri/src/handlers/home.rs +++ b/src-tauri/src/handlers/home.rs @@ -1,15 +1,15 @@ +use super::server::get_json_server_url; use crate::handlers::server::get_img_server_url; use crate::models::home::{HomeLink, HomeList, HomeListApp}; -use crate::utils::UA; use crate::utils::format::format_icon_url; -use super::server::get_json_server_url; +use crate::utils::UA; #[tauri::command] pub async fn get_home_links() -> Result, String> { let json_server_url = get_json_server_url(); let img_server_url = get_img_server_url(); let url = format!("{}/home/homelinks.json", json_server_url); - + let client = reqwest::Client::new(); let response = client .get(&url) @@ -17,22 +17,19 @@ pub async fn get_home_links() -> Result, String> { .send() .await .map_err(|e| e.to_string())?; - + // 获取响应内容的文本形式 - let response_text = response - .text() - .await - .map_err(|e| e.to_string())?; - + let response_text = response.text().await.map_err(|e| e.to_string())?; + // 将文本解析为 HomeLink 向量 - let mut links: Vec = serde_json::from_str(&response_text) - .map_err(|e| e.to_string())?; - + let mut links: Vec = + serde_json::from_str(&response_text).map_err(|e| e.to_string())?; + // 为每个 HomeLink 的 img_url 添加图片服务器前缀 for link in &mut links { link.img_url = format!("{}{}", img_server_url, link.img_url); } - + Ok(links) } @@ -40,7 +37,7 @@ pub async fn get_home_links() -> Result, String> { pub async fn get_home_list_apps(json_url: String) -> Result, String> { let json_server_url = get_json_server_url(); let url = format!("{}{}", json_server_url, json_url); - + let client = reqwest::Client::new(); let response = client .get(&url) @@ -48,19 +45,16 @@ pub async fn get_home_list_apps(json_url: String) -> Result, St .send() .await .map_err(|e| e.to_string())?; - - let response_text = response - .text() - .await - .map_err(|e| e.to_string())?; - - let mut apps: Vec = serde_json::from_str(&response_text) - .map_err(|e| e.to_string())?; + + let response_text = response.text().await.map_err(|e| e.to_string())?; + + let mut apps: Vec = + serde_json::from_str(&response_text).map_err(|e| e.to_string())?; for app in &mut apps { app.icon = Some(format_icon_url(&app.category, &app.pkgname)); } - + Ok(apps) } @@ -68,7 +62,7 @@ pub async fn get_home_list_apps(json_url: String) -> Result, St pub async fn get_home_lists() -> Result, String> { let json_server_url = get_json_server_url(); let url = format!("{}/home/homelist.json", json_server_url); - + let client = reqwest::Client::new(); let response = client .get(&url) @@ -76,14 +70,10 @@ pub async fn get_home_lists() -> Result, String> { .send() .await .map_err(|e| e.to_string())?; - - let response_text = response - .text() - .await - .map_err(|e| e.to_string())?; - - let lists: Vec = serde_json::from_str(&response_text) - .map_err(|e| e.to_string())?; - + + let response_text = response.text().await.map_err(|e| e.to_string())?; + + let lists: Vec = serde_json::from_str(&response_text).map_err(|e| e.to_string())?; + Ok(lists) -} \ No newline at end of file +} diff --git a/src-tauri/src/handlers/mod.rs b/src-tauri/src/handlers/mod.rs index 31ed4db..62ac05a 100644 --- a/src-tauri/src/handlers/mod.rs +++ b/src-tauri/src/handlers/mod.rs @@ -1,7 +1,7 @@ -pub mod category; -pub mod server; pub mod app; -pub mod home; -pub mod file; +pub mod category; +pub mod deb; pub mod download; -pub mod deb; \ No newline at end of file +pub mod file; +pub mod home; +pub mod server; diff --git a/src-tauri/src/handlers/server.rs b/src-tauri/src/handlers/server.rs index 4b3e488..e46b477 100644 --- a/src-tauri/src/handlers/server.rs +++ b/src-tauri/src/handlers/server.rs @@ -26,4 +26,4 @@ pub fn get_json_server_url() -> String { pub fn get_img_server_url() -> String { let arch = get_target_arch_to_store(); return format!("https://spk-json.spark-app.store/{}/", arch); -} \ No newline at end of file +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e5f800a..8fc56a9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,13 +1,14 @@ -use utils::download_manager::DownloadManager; use tauri::Manager; +use utils::download_manager::DownloadManager; -mod models; mod handlers; +mod models; mod utils; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() + .plugin(tauri_plugin_window_state::Builder::new().build()) .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_clipboard_manager::init()) .manage(utils::download_manager::DownloadManager::new()) @@ -34,7 +35,7 @@ pub fn run() { handlers::deb::check_launch_app, handlers::deb::launch_launch_app, utils::get_user_agent, - ]) + ]) .on_window_event(|window, event| match event { tauri::WindowEvent::Destroyed => { // 获取 DownloadManager 实例并关闭 aria2 @@ -57,7 +58,7 @@ pub fn run() { tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; } }); - + Ok(()) }) .run(tauri::generate_context!()) diff --git a/src-tauri/src/models/app.rs b/src-tauri/src/models/app.rs index 55e780c..cd65586 100644 --- a/src-tauri/src/models/app.rs +++ b/src-tauri/src/models/app.rs @@ -48,4 +48,4 @@ pub struct AppDetail { pub size: String, #[serde(rename = "DownloadTimes")] pub download_times: Option, -} \ No newline at end of file +} diff --git a/src-tauri/src/models/category.rs b/src-tauri/src/models/category.rs index c69b36c..186c087 100644 --- a/src-tauri/src/models/category.rs +++ b/src-tauri/src/models/category.rs @@ -1,8 +1,8 @@ use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize,Clone,Debug)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub struct Category { pub id: String, pub icon: String, pub name_zh_cn: String, -} \ No newline at end of file +} diff --git a/src-tauri/src/models/download.rs b/src-tauri/src/models/download.rs index deb4737..2443c64 100644 --- a/src-tauri/src/models/download.rs +++ b/src-tauri/src/models/download.rs @@ -34,20 +34,20 @@ pub struct DownloadTaskResponse { #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "lowercase")] pub enum ResponseStatus { - Downloading, // 下载中 - Queued, // 等待下载 - Paused, // 下载暂停 - Completed, // 下载完成 - Error, // 错误 - Installing, // 安装中 - Installed, // 安装完成 + Downloading, // 下载中 + Queued, // 等待下载 + Paused, // 下载暂停 + Completed, // 下载完成 + Error, // 错误 + Installing, // 安装中 + Installed, // 安装完成 } #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "lowercase")] pub enum InstallStatus { - Queued, // 等待安装 - Error, // 错误 - Installing, // 安装中 - Installed, // 安装完成 -} \ No newline at end of file + Queued, // 等待安装 + Error, // 错误 + Installing, // 安装中 + Installed, // 安装完成 +} diff --git a/src-tauri/src/models/home.rs b/src-tauri/src/models/home.rs index 37085dc..595964c 100644 --- a/src-tauri/src/models/home.rs +++ b/src-tauri/src/models/home.rs @@ -32,4 +32,4 @@ pub struct HomeList { pub list_type: String, #[serde(rename = "jsonUrl")] pub json_url: String, -} \ No newline at end of file +} diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs index 89f75a3..eb998d0 100644 --- a/src-tauri/src/models/mod.rs +++ b/src-tauri/src/models/mod.rs @@ -1,4 +1,4 @@ -pub mod category; pub mod app; +pub mod category; +pub mod download; pub mod home; -pub mod download; \ No newline at end of file diff --git a/src-tauri/src/utils/aria2.rs b/src-tauri/src/utils/aria2.rs index 20bc04f..ea74e40 100644 --- a/src-tauri/src/utils/aria2.rs +++ b/src-tauri/src/utils/aria2.rs @@ -45,17 +45,17 @@ impl Aria2Client { } /// 添加Metalink下载 - /// + /// /// 通过上传.metalink文件内容添加Metalink下载 - /// + /// /// # 参数 - /// + /// /// * `metalink` - metalink文件的内容 /// * `options` - 可选的下载选项 /// * `position` - 可选的队列位置 - /// + /// /// # 返回 - /// + /// /// 返回新注册下载的GID数组 pub async fn add_metalink( &self, @@ -65,31 +65,31 @@ impl Aria2Client { ) -> Result, Box> { // Base64编码metalink内容 let encoded_metalink = general_purpose::STANDARD.encode(metalink); - + // 构建参数列表 let mut params = Vec::new(); - + // 如果有密钥,添加到参数列表 if let Some(secret) = &self.secret { params.push(json!(secret)); } - + // 添加metalink内容 params.push(json!(encoded_metalink)); - + // 添加选项(如果有) if let Some(opts) = options { params.push(json!(opts)); } - + // 添加位置(如果有) if let Some(pos) = position { params.push(json!(pos)); } - + // 发送RPC请求 let response = self.send_request("aria2.addMetalink", params).await?; - + // 解析结果 if let Some(result) = response.result { if let Some(gids) = result.as_array() { @@ -100,14 +100,14 @@ impl Aria2Client { return Ok(gid_vec); } } - + if let Some(error) = response.error { return Err(format!("Aria2 RPC错误: {} ({})", error.message, error.code).into()); } - + Err("无法解析Aria2响应".into()) } - + /// 发送JSON-RPC请求到aria2 async fn send_request( &self, @@ -120,246 +120,255 @@ impl Aria2Client { method: method.to_string(), params, }; - - let response = self.client + + let response = self + .client .post(&self.rpc_url) .json(&request) .send() .await? .json::() .await?; - + Ok(response) } - + /// 获取下载状态 pub async fn tell_status(&self, gid: &str) -> Result> { let mut params = Vec::new(); - + if let Some(secret) = &self.secret { params.push(json!(secret)); } - + params.push(json!(gid)); - + let response = self.send_request("aria2.tellStatus", params).await?; - + if let Some(result) = response.result { return Ok(result); } - + if let Some(error) = response.error { return Err(format!("Aria2 RPC错误: {} ({})", error.message, error.code).into()); } - + Err("无法获取下载状态".into()) } - + /// 获取所有活动中的下载任务 - /// + /// /// 返回所有正在下载的任务的状态信息 - /// + /// /// # 返回 - /// + /// /// 返回包含所有活动下载任务信息的数组 pub async fn tell_active(&self) -> Result, Box> { let mut params = Vec::new(); - + if let Some(secret) = &self.secret { params.push(json!(secret)); } - + let response = self.send_request("aria2.tellActive", params).await?; - + if let Some(result) = response.result { if let Some(downloads) = result.as_array() { return Ok(downloads.clone()); } } - + if let Some(error) = response.error { return Err(format!("Aria2 RPC错误: {} ({})", error.message, error.code).into()); } - + Err("无法获取活动下载任务".into()) } - + /// 获取等待中的下载任务 - /// + /// /// 返回等待队列中的下载任务状态信息 - /// + /// /// # 参数 - /// + /// /// * `offset` - 从队列开始位置的偏移量 /// * `num` - 要获取的任务数量 - /// + /// /// # 返回 - /// + /// /// 返回包含等待中下载任务信息的数组 - pub async fn tell_waiting(&self, offset: usize, num: usize) -> Result, Box> { + pub async fn tell_waiting( + &self, + offset: usize, + num: usize, + ) -> Result, Box> { let mut params = Vec::new(); - + if let Some(secret) = &self.secret { params.push(json!(secret)); } - + params.push(json!(offset)); params.push(json!(num)); - + let response = self.send_request("aria2.tellWaiting", params).await?; - + if let Some(result) = response.result { if let Some(downloads) = result.as_array() { return Ok(downloads.clone()); } } - + if let Some(error) = response.error { return Err(format!("Aria2 RPC错误: {} ({})", error.message, error.code).into()); } - + Err("无法获取等待中的下载任务".into()) } - + /// 获取已停止的下载任务 - /// + /// /// 返回已完成/错误/已移除的下载任务状态信息 - /// + /// /// # 参数 - /// + /// /// * `offset` - 从队列开始位置的偏移量 /// * `num` - 要获取的任务数量 - /// + /// /// # 返回 - /// + /// /// 返回包含已停止下载任务信息的数组 - pub async fn tell_stopped(&self, offset: usize, num: usize) -> Result, Box> { + pub async fn tell_stopped( + &self, + offset: usize, + num: usize, + ) -> Result, Box> { let mut params = Vec::new(); - + if let Some(secret) = &self.secret { params.push(json!(secret)); } - + params.push(json!(offset)); params.push(json!(num)); - + let response = self.send_request("aria2.tellStopped", params).await?; - + if let Some(result) = response.result { if let Some(downloads) = result.as_array() { return Ok(downloads.clone()); } } - + if let Some(error) = response.error { return Err(format!("Aria2 RPC错误: {} ({})", error.message, error.code).into()); } - + Err("无法获取已停止的下载任务".into()) } /// 暂停下载任务 - /// + /// /// 暂停指定GID的下载任务 - /// + /// /// # 参数 - /// + /// /// * `gid` - 下载任务的GID - /// + /// /// # 返回 - /// + /// /// 成功时返回被暂停的GID pub async fn pause(&self, gid: &str) -> Result> { let mut params = Vec::new(); - + if let Some(secret) = &self.secret { params.push(json!(secret)); } - + params.push(json!(gid)); - + let response = self.send_request("aria2.pause", params).await?; - + if let Some(result) = response.result { if let Some(paused_gid) = result.as_str() { return Ok(paused_gid.to_string()); } } - + if let Some(error) = response.error { return Err(format!("Aria2 RPC错误: {} ({})", error.message, error.code).into()); } - + Err("无法暂停下载任务".into()) } - + /// 恢复下载任务 - /// + /// /// 恢复指定GID的下载任务 - /// + /// /// # 参数 - /// + /// /// * `gid` - 下载任务的GID - /// + /// /// # 返回 - /// + /// /// 成功时返回被恢复的GID pub async fn unpause(&self, gid: &str) -> Result> { let mut params = Vec::new(); - + if let Some(secret) = &self.secret { params.push(json!(secret)); } - + params.push(json!(gid)); - + let response = self.send_request("aria2.unpause", params).await?; - + if let Some(result) = response.result { if let Some(unpaused_gid) = result.as_str() { return Ok(unpaused_gid.to_string()); } } - + if let Some(error) = response.error { return Err(format!("Aria2 RPC错误: {} ({})", error.message, error.code).into()); } - + Err("无法恢复下载任务".into()) } - + /// 删除下载任务 - /// + /// /// 删除指定GID的下载任务 - /// + /// /// # 参数 - /// + /// /// * `gid` - 下载任务的GID - /// + /// /// # 返回 - /// + /// /// 成功时返回被删除的GID pub async fn remove(&self, gid: &str) -> Result> { let mut params = Vec::new(); - + if let Some(secret) = &self.secret { params.push(json!(secret)); } - + params.push(json!(gid)); - + let response = self.send_request("aria2.remove", params).await?; - + if let Some(result) = response.result { if let Some(removed_gid) = result.as_str() { return Ok(removed_gid.to_string()); } } - + if let Some(error) = response.error { return Err(format!("Aria2 RPC错误: {} ({})", error.message, error.code).into()); } - + Err("无法删除下载任务".into()) } -} \ No newline at end of file +} diff --git a/src-tauri/src/utils/download_manager.rs b/src-tauri/src/utils/download_manager.rs index 39def81..c826286 100644 --- a/src-tauri/src/utils/download_manager.rs +++ b/src-tauri/src/utils/download_manager.rs @@ -1,14 +1,16 @@ +use crate::handlers::server::get_json_server_url; +use crate::models::download::{ + DownloadTask, DownloadTaskResponse, InstallStatus, InstallTask, ResponseStatus, +}; +use crate::utils::{aria2::Aria2Client, UA}; use std::collections::HashMap; -use std::sync::Mutex; +use std::net::TcpListener; use std::process::Command; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; -use std::net::TcpListener; -use crate::models::download::{DownloadTask, DownloadTaskResponse, InstallStatus, InstallTask, ResponseStatus}; -use crate::handlers::server::get_json_server_url; -use crate::utils::{UA, aria2::Aria2Client}; +use std::sync::Mutex; -use super::format::{format_size, format_speed, format_icon_url}; +use super::format::{format_icon_url, format_size, format_speed}; pub struct DownloadManager { download_queue: Mutex>, @@ -28,7 +30,7 @@ impl DownloadManager { aria2_started: Arc::new(AtomicBool::new(false)), aria2_port: Arc::new(Mutex::new(5144)), aria2_pid: Arc::new(Mutex::new(None)), - installing: Arc::new(AtomicBool::new(false)), // 初始化为 false + installing: Arc::new(AtomicBool::new(false)), // 初始化为 false last_get_downloads: Arc::new(Mutex::new(None)), } } @@ -67,59 +69,68 @@ impl DownloadManager { let tasks_clone: Vec; let aria2_started; let port; - + { // 使用作用域限制锁的生命周期 let downloads = self.download_queue.lock().map_err(|e| e.to_string())?; tasks_clone = downloads.values().cloned().collect(); aria2_started = self.aria2_started.load(Ordering::SeqCst); } - + // 如果 aria2 未启动,直接返回队列中的任务 if !aria2_started { - return Ok(tasks_clone.into_iter().map(|task| DownloadTaskResponse { - category: task.category, - pkgname: task.pkgname, - filename: task.filename, - status: ResponseStatus::Error, // 如果 aria2 未启动,标记为错误状态 - icon: task.icon, - name: task.name, - progress: 0.0, - speed: None, - size: None, - }).collect()); + return Ok(tasks_clone + .into_iter() + .map(|task| DownloadTaskResponse { + category: task.category, + pkgname: task.pkgname, + filename: task.filename, + status: ResponseStatus::Error, // 如果 aria2 未启动,标记为错误状态 + icon: task.icon, + name: task.name, + progress: 0.0, + speed: None, + size: None, + }) + .collect()); } - + // 获取端口(在单独的作用域中获取锁) { port = *self.aria2_port.lock().map_err(|e| e.to_string())?; } - + // 创建 Aria2Client 实例 let aria2_client = Aria2Client::new("127.0.0.1", port, None); - + // 获取所有活动中的下载任务 - let active_downloads = aria2_client.tell_active().await + let active_downloads = aria2_client + .tell_active() + .await .map_err(|e| format!("获取活动下载任务失败: {}", e))?; - + // 获取所有等待中的下载任务(最多100个) - let waiting_downloads = aria2_client.tell_waiting(0, 100).await + let waiting_downloads = aria2_client + .tell_waiting(0, 100) + .await .map_err(|e| format!("获取等待中下载任务失败: {}", e))?; - + // 获取所有已停止的下载任务(最多100个) - let stopped_downloads = aria2_client.tell_stopped(0, 100).await + let stopped_downloads = aria2_client + .tell_stopped(0, 100) + .await .map_err(|e| format!("获取已停止下载任务失败: {}", e))?; - + // 创建一个映射,用于存储 GID 到 aria2 任务状态的映射 let mut aria2_tasks = HashMap::new(); - + // 处理活动中的下载任务 for task in active_downloads { if let Some(gid) = task["gid"].as_str() { aria2_tasks.insert(gid.to_string(), (task, ResponseStatus::Downloading)); } } - + // 处理等待中的下载任务 for task in waiting_downloads { if let Some(gid) = task["gid"].as_str() { @@ -131,7 +142,7 @@ impl DownloadManager { aria2_tasks.insert(gid.to_string(), (task, status)); } } - + // 处理已停止的下载任务 for task in stopped_downloads { if let Some(gid) = task["gid"].as_str() { @@ -139,15 +150,14 @@ impl DownloadManager { // 当任务完成时,将其加入到安装队列 let task_info = { let downloads = self.download_queue.lock().map_err(|e| e.to_string())?; - downloads.values() - .find(|t| t.gid == gid) - .cloned() + downloads.values().find(|t| t.gid == gid).cloned() }; if let Some(task_info) = task_info { let task_id = format!("{}/{}", task_info.category, task_info.pkgname); let should_process = { - let mut install_queue = self.install_queue.lock().map_err(|e| e.to_string())?; + let mut install_queue = + self.install_queue.lock().map_err(|e| e.to_string())?; if !install_queue.contains_key(&task_id) { let install_task = InstallTask { category: task_info.category.clone(), @@ -174,16 +184,16 @@ impl DownloadManager { aria2_tasks.insert(gid.to_string(), (task, status)); } } - + // 将队列中的任务与 aria2 任务状态结合,生成响应 let mut result = Vec::new(); - + // 获取安装队列(在生成响应之前) let install_queue = self.install_queue.lock().map_err(|e| e.to_string())?; - + for task in tasks_clone { let task_id = format!("{}/{}", task.category, task.pkgname); - + let mut response = DownloadTaskResponse { category: task.category.clone(), pkgname: task.pkgname.clone(), @@ -195,7 +205,7 @@ impl DownloadManager { speed: None, size: None, }; - + // 首先检查是否在安装队列中 if let Some(install_task) = install_queue.get(&task_id) { response.status = match install_task.status { @@ -208,25 +218,29 @@ impl DownloadManager { // 如果不在安装队列中,则检查下载状态 if let Some((aria2_task, status)) = aria2_tasks.get(&task.gid) { response.status = status.clone(); - + // 计算进度(百分比) if let (Some(completed_length), Some(total_length)) = ( - aria2_task["completedLength"].as_str().and_then(|s| s.parse::().ok()), - aria2_task["totalLength"].as_str().and_then(|s| s.parse::().ok()) + aria2_task["completedLength"] + .as_str() + .and_then(|s| s.parse::().ok()), + aria2_task["totalLength"] + .as_str() + .and_then(|s| s.parse::().ok()), ) { if total_length > 0.0 { let progress = ((completed_length / total_length) * 100.0) as f32; response.progress = (progress * 100.0).round() / 100.0; } } - + // 获取下载速度 if let Some(download_speed) = aria2_task["downloadSpeed"].as_str() { if let Ok(speed) = download_speed.parse::() { response.speed = Some(format_speed(speed)); } } - + // 获取文件大小 if let Some(total_length) = aria2_task["totalLength"].as_str() { if let Ok(size) = total_length.parse::() { @@ -235,10 +249,10 @@ impl DownloadManager { } } } - + result.push(response); } - + Ok(result) } @@ -263,7 +277,8 @@ impl DownloadManager { "--dir=/tmp/spark-store", // 设置下载目录为 /tmp/spark-store ]) .spawn() - .map_err(|e| format!("启动 aria2 失败: {}", e)).unwrap(); + .map_err(|e| format!("启动 aria2 失败: {}", e)) + .unwrap(); // 保存进程 ID if let Ok(mut pid_guard) = self.aria2_pid.lock() { @@ -274,17 +289,26 @@ impl DownloadManager { } // 添加下载任务 - pub async fn add_download(&self, category: String, pkgname: String, filename: String, name: String) -> Result<(), String> { + pub async fn add_download( + &self, + category: String, + pkgname: String, + filename: String, + name: String, + ) -> Result<(), String> { // 检查并启动 aria2(如果还没启动) if !self.aria2_started.load(Ordering::SeqCst) { self.start_aria2(); } let task_id = format!("{}/{}", category, pkgname); - + // 获取metalink文件URL和内容(在获取锁之前完成) let json_server_url = get_json_server_url(); - let metalink_url = format!("{}{}/{}/{}.metalink", json_server_url, category, pkgname, filename); + let metalink_url = format!( + "{}{}/{}/{}.metalink", + json_server_url, category, pkgname, filename + ); // 发送请求获取metalink文件 let client = reqwest::Client::new(); @@ -294,33 +318,31 @@ impl DownloadManager { .send() .await .map_err(|e| format!("获取metalink文件失败: {}", e))?; - + // 检查响应状态 if !response.status().is_success() { return Err(format!("获取metalink文件失败: HTTP {}", response.status())); } // 获取metalink文件内容 - let metalink_content = response.bytes() - .await - .map_err(|e| format!("读取metalink内容失败: {}", e))?; - + let metalink_content = response + .bytes() + .await + .map_err(|e| format!("读取metalink内容失败: {}", e))?; + // 创建Aria2Client并添加下载任务 let port = *self.aria2_port.lock().map_err(|e| e.to_string())?; let aria2_client = Aria2Client::new("127.0.0.1", port, None); - + // 使用Aria2Client添加metalink下载 - let gids = aria2_client.add_metalink( - &metalink_content, - None, - None - ) - .await - .map_err(|e| format!("添加下载任务失败: {}", e))?; + let gids = aria2_client + .add_metalink(&metalink_content, None, None) + .await + .map_err(|e| format!("添加下载任务失败: {}", e))?; // 获取一次锁,完成所有状态更新操作 let mut downloads = self.download_queue.lock().map_err(|e| e.to_string())?; - + // 检查任务是否已存在 if downloads.contains_key(&task_id) { return Ok(()); @@ -333,19 +355,19 @@ impl DownloadManager { filename, name, icon: format_icon_url(&category, &pkgname), - gid: gids[0].clone() + gid: gids[0].clone(), }; - + // 添加任务到队列 downloads.insert(task_id, task); - + Ok(()) } // 暂停下载任务 pub async fn pause_download(&self, category: String, pkgname: String) -> Result<(), String> { let task_id = format!("{}/{}", category, pkgname); - + // 获取任务信息,并在作用域结束时释放锁 let task_gid = { let downloads = self.download_queue.lock().map_err(|e| e.to_string())?; @@ -354,27 +376,29 @@ impl DownloadManager { None => return Err(format!("找不到下载任务: {}", task_id)), } }; - + // 如果 aria2 未启动,返回错误 if !self.aria2_started.load(Ordering::SeqCst) { return Err("aria2 未启动".to_string()); } - + // 创建 Aria2Client 实例 let port = *self.aria2_port.lock().map_err(|e| e.to_string())?; let aria2_client = Aria2Client::new("127.0.0.1", port, None); - + // 调用 aria2 的 pause 方法 - aria2_client.pause(task_gid.as_str()).await + aria2_client + .pause(task_gid.as_str()) + .await .map_err(|e| format!("暂停下载任务失败: {}", e))?; - + Ok(()) } // 恢复下载任务 pub async fn resume_download(&self, category: String, pkgname: String) -> Result<(), String> { let task_id = format!("{}/{}", category, pkgname); - + // 获取任务信息,并在作用域结束时释放锁 let task_gid = { let downloads = self.download_queue.lock().map_err(|e| e.to_string())?; @@ -383,27 +407,29 @@ impl DownloadManager { None => return Err(format!("找不到下载任务: {}", task_id)), } }; - + // 如果 aria2 未启动,返回错误 if !self.aria2_started.load(Ordering::SeqCst) { return Err("aria2 未启动".to_string()); } - + // 创建 Aria2Client 实例 let port = *self.aria2_port.lock().map_err(|e| e.to_string())?; let aria2_client = Aria2Client::new("127.0.0.1", port, None); - + // 调用 aria2 的 unpause 方法 - aria2_client.unpause(task_gid.as_str()).await + aria2_client + .unpause(task_gid.as_str()) + .await .map_err(|e| format!("恢复下载任务失败: {}", e))?; - + Ok(()) } // 取消下载任务 pub async fn cancel_download(&self, category: String, pkgname: String) -> Result<(), String> { let task_id = format!("{}/{}", category, pkgname); - + // 获取任务信息,并在作用域结束时释放锁 let task_gid = { let mut downloads = self.download_queue.lock().map_err(|e| e.to_string())?; @@ -415,23 +441,25 @@ impl DownloadManager { return Ok(()); } task.gid.clone() - }, + } None => return Err(format!("找不到下载任务: {}", task_id)), } }; - + // 创建 Aria2Client 实例 let port = *self.aria2_port.lock().map_err(|e| e.to_string())?; let aria2_client = Aria2Client::new("127.0.0.1", port, None); - + // 调用 aria2 的 remove 方法 - aria2_client.remove(task_gid.as_str()).await + aria2_client + .remove(task_gid.as_str()) + .await .map_err(|e| format!("取消下载任务失败: {}", e))?; - + // 从队列中移除任务 let mut downloads = self.download_queue.lock().map_err(|e| e.to_string())?; downloads.remove(&task_id); - + Ok(()) } @@ -442,9 +470,7 @@ impl DownloadManager { if let Ok(pid_guard) = self.aria2_pid.lock() { if let Some(pid) = *pid_guard { // 使用 kill 命令终止特定的进程 - if let Ok(output) = Command::new("kill") - .arg(pid.to_string()) - .output() { + if let Ok(output) = Command::new("kill").arg(pid.to_string()).output() { if output.status.success() { println!("成功关闭 aria2 (PID: {})", pid); } else { @@ -467,7 +493,8 @@ impl DownloadManager { // 查找第一个等待安装的任务 let (task_id, task) = { let mut install_queue = self.install_queue.lock().map_err(|e| e.to_string())?; - if let Some((id, task)) = install_queue.iter_mut() + if let Some((id, task)) = install_queue + .iter_mut() .find(|(_, task)| matches!(task.status, InstallStatus::Queued)) .map(|(id, task)| (id.clone(), task.clone())) { @@ -512,7 +539,7 @@ impl DownloadManager { // 实际执行安装的方法 async fn install_package(&self, task: &InstallTask) -> Result<(), String> { println!("开始安装包: {}", task.filepath); - + // 移除可能存在的引号 let filepath = task.filepath.trim_matches('"'); @@ -546,4 +573,4 @@ impl DownloadManager { false } } -} \ No newline at end of file +} diff --git a/src-tauri/src/utils/format.rs b/src-tauri/src/utils/format.rs index 8d26ffb..1fc1093 100644 --- a/src-tauri/src/utils/format.rs +++ b/src-tauri/src/utils/format.rs @@ -5,7 +5,7 @@ pub fn format_size(size: u64) -> String { const KB: u64 = 1024; const MB: u64 = KB * 1024; const GB: u64 = MB * 1024; - + if size >= GB { format!("{:.2} GB", size as f64 / GB as f64) } else if size >= MB { @@ -24,4 +24,4 @@ pub fn format_speed(speed: u64) -> String { pub fn format_icon_url(category: &str, pkgname: &str) -> String { format!("{}{}/{}/icon.png", get_img_server_url(), category, pkgname) -} \ No newline at end of file +} diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index d714d6d..a626d5e 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -1,11 +1,11 @@ -pub mod search; -pub mod download_manager; pub mod aria2; +pub mod download_manager; pub mod format; +pub mod search; pub const UA: &str = concat!("Spark-Store/", env!("CARGO_PKG_VERSION")); #[tauri::command] pub fn get_user_agent() -> String { UA.into() -} \ No newline at end of file +} diff --git a/src-tauri/src/utils/search.rs b/src-tauri/src/utils/search.rs index d0fff41..1be0966 100644 --- a/src-tauri/src/utils/search.rs +++ b/src-tauri/src/utils/search.rs @@ -4,19 +4,19 @@ use std::collections::HashSet; pub fn match_text(text: &str, query: &str) -> bool { let text_lower = text.to_lowercase(); let query_lower = query.to_lowercase(); - + // 直接匹配原文 if text_lower.contains(&query_lower) { return true; } - + // 获取文本的拼音并转换为小写 let text_pinyin: String = text .to_pinyin() .filter_map(|p| p) .map(|p| p.plain().to_lowercase()) .collect(); - + // 获取查询的拼音并转换为小写 // 判断查询是否为纯英文 let query_pinyin = if query.chars().all(|c| c.is_ascii_alphabetic()) { @@ -28,33 +28,37 @@ pub fn match_text(text: &str, query: &str) -> bool { .map(|p| p.plain().to_lowercase()) .collect() }; - + // 匹配拼音 text_pinyin.contains(&query_pinyin) } -pub fn search_apps(apps: &[crate::models::app::AppItem], query: &str) -> Vec { +pub fn search_apps( + apps: &[crate::models::app::AppItem], + query: &str, +) -> Vec { if query.is_empty() { return apps.to_vec(); } - + let mut results = Vec::new(); let mut seen = HashSet::new(); - + for app in apps { // 避免重复 if seen.contains(&app.pkgname) { continue; } - + // 匹配名称、包名和描述 - if match_text(&app.name, query) || - match_text(&app.pkgname, query) || - match_text(&app.more, query) { + if match_text(&app.name, query) + || match_text(&app.pkgname, query) + || match_text(&app.more, query) + { results.push(app.clone()); seen.insert(app.pkgname.clone()); } } - + results -} \ No newline at end of file +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 93bf87a..1f50c62 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -14,7 +14,9 @@ { "title": "spark-store", "width": 800, - "height": 600 + "height": 600, + "minWidth": 355, + "minHeight": 510 } ], "security": { diff --git a/yarn.lock b/yarn.lock index 599da2e..805195e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -702,6 +702,13 @@ dependencies: "@tauri-apps/api" "^2.0.0" +"@tauri-apps/plugin-window-state@~2": + version "2.2.1" + resolved "https://registry.npmmirror.com/@tauri-apps/plugin-window-state/-/plugin-window-state-2.2.1.tgz#11bbc9d4445c0ef31893198b2d9d949c4f02a454" + integrity sha512-L7FhG/ocQNt8t+TMBkvl8eLhCU6I19t848unKMUgNHuvwHPaurzZr4knulNyKzqz7zVYSz9AdvgWy4915eq+AA== + dependencies: + "@tauri-apps/api" "^2.0.0" + "@types/babel__core@^7.20.4": version "7.20.5" resolved "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017"