diff --git a/src-tauri/src/models/download.rs b/src-tauri/src/models/download.rs index 53cb1b1..deb4737 100644 --- a/src-tauri/src/models/download.rs +++ b/src-tauri/src/models/download.rs @@ -10,12 +10,20 @@ pub struct DownloadTask { pub gid: String, } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct InstallTask { + pub category: String, + pub pkgname: String, + pub filepath: String, + pub status: InstallStatus, +} + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct DownloadTaskResponse { pub category: String, pub pkgname: String, pub filename: String, - pub status: DownloadStatus, + pub status: ResponseStatus, pub icon: String, pub name: String, pub progress: f32, @@ -25,10 +33,21 @@ pub struct DownloadTaskResponse { #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "lowercase")] -pub enum DownloadStatus { - Downloading, - Queued, - Paused, - Completed, - Error, +pub enum ResponseStatus { + 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 diff --git a/src-tauri/src/utils/aria2.rs b/src-tauri/src/utils/aria2.rs index fec4144..20bc04f 100644 --- a/src-tauri/src/utils/aria2.rs +++ b/src-tauri/src/utils/aria2.rs @@ -4,7 +4,6 @@ use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::collections::HashMap; use std::error::Error; -use std::sync::Arc; #[derive(Debug, Clone)] pub struct Aria2Client { diff --git a/src-tauri/src/utils/download_manager.rs b/src-tauri/src/utils/download_manager.rs index 06ced88..6730161 100644 --- a/src-tauri/src/utils/download_manager.rs +++ b/src-tauri/src/utils/download_manager.rs @@ -4,26 +4,30 @@ use std::process::Command; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::net::TcpListener; -use crate::models::download::{DownloadStatus, DownloadTask, DownloadTaskResponse}; +use crate::models::download::{DownloadTask, DownloadTaskResponse, InstallStatus, InstallTask, ResponseStatus}; use crate::handlers::server::get_json_server_url; use crate::utils::{format_icon_url, UA, aria2::Aria2Client}; use super::format::{format_size, format_speed}; pub struct DownloadManager { - queue: Mutex>, + download_queue: Mutex>, + install_queue: Mutex>, aria2_started: Arc, aria2_port: Arc>, aria2_pid: Arc>>, + installing: Arc, // 新增:标记是否有正在安装的任务 } impl DownloadManager { pub fn new() -> Self { DownloadManager { - queue: Mutex::new(HashMap::new()), + download_queue: Mutex::new(HashMap::new()), + install_queue: Mutex::new(HashMap::new()), aria2_started: Arc::new(AtomicBool::new(false)), aria2_port: Arc::new(Mutex::new(5144)), - aria2_pid: Arc::new(Mutex::new(None)), + aria2_pid: Arc::new(Mutex::new(None)), + installing: Arc::new(AtomicBool::new(false)), // 初始化为 false } } @@ -36,7 +40,7 @@ impl DownloadManager { { // 使用作用域限制锁的生命周期 - let downloads = self.queue.lock().map_err(|e| e.to_string())?; + 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); } @@ -47,7 +51,7 @@ impl DownloadManager { category: task.category, pkgname: task.pkgname, filename: task.filename, - status: DownloadStatus::Error, // 如果 aria2 未启动,标记为错误状态 + status: ResponseStatus::Error, // 如果 aria2 未启动,标记为错误状态 icon: task.icon, name: task.name, progress: 0.0, @@ -82,7 +86,7 @@ impl DownloadManager { // 处理活动中的下载任务 for task in active_downloads { if let Some(gid) = task["gid"].as_str() { - aria2_tasks.insert(gid.to_string(), (task, DownloadStatus::Downloading)); + aria2_tasks.insert(gid.to_string(), (task, ResponseStatus::Downloading)); } } @@ -90,9 +94,9 @@ impl DownloadManager { for task in waiting_downloads { if let Some(gid) = task["gid"].as_str() { let status = if task["status"].as_str() == Some("paused") { - DownloadStatus::Paused + ResponseStatus::Paused } else { - DownloadStatus::Queued + ResponseStatus::Queued }; aria2_tasks.insert(gid.to_string(), (task, status)); } @@ -102,9 +106,40 @@ impl DownloadManager { for task in stopped_downloads { if let Some(gid) = task["gid"].as_str() { let status = if task["status"].as_str() == Some("complete") { - DownloadStatus::Completed + // 当任务完成时,将其加入到安装队列 + let task_info = { + let downloads = self.download_queue.lock().map_err(|e| e.to_string())?; + 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())?; + if !install_queue.contains_key(&task_id) { + let install_task = InstallTask { + category: task_info.category.clone(), + pkgname: task_info.pkgname.clone(), + filepath: task["files"][0]["path"].to_string(), + status: InstallStatus::Queued, + }; + install_queue.insert(task_id, install_task); + true + } else { + false + } + }; + + // 在所有锁都释放后再处理安装队列 + if should_process { + let _ = self.process_install_queue().await; + } + } + ResponseStatus::Completed } else { - DownloadStatus::Error + ResponseStatus::Error }; aria2_tasks.insert(gid.to_string(), (task, status)); } @@ -113,12 +148,17 @@ impl DownloadManager { // 将队列中的任务与 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(), filename: task.filename.clone(), - status: DownloadStatus::Error, // 默认为错误状态 + status: ResponseStatus::Error, // 默认为错误状态 icon: task.icon.clone(), name: task.name.clone(), progress: 0.0, @@ -126,33 +166,42 @@ impl DownloadManager { size: None, }; - // 如果在 aria2 任务中找到对应的 GID,更新状态信息 - 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()) - ) { - 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(install_task) = install_queue.get(&task_id) { + response.status = match install_task.status { + InstallStatus::Queued => ResponseStatus::Completed, + InstallStatus::Installing => ResponseStatus::Installing, + InstallStatus::Installed => ResponseStatus::Installed, + InstallStatus::Error => ResponseStatus::Error, + }; + } else { + // 如果不在安装队列中,则检查下载状态 + 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()) + ) { + 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(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::() { - response.size = Some(format_size(size)); + + // 获取文件大小 + if let Some(total_length) = aria2_task["totalLength"].as_str() { + if let Ok(size) = total_length.parse::() { + response.size = Some(format_size(size)); + } } } } @@ -238,7 +287,7 @@ impl DownloadManager { .map_err(|e| format!("添加下载任务失败: {}", e))?; // 获取一次锁,完成所有状态更新操作 - let mut downloads = self.queue.lock().map_err(|e| e.to_string())?; + let mut downloads = self.download_queue.lock().map_err(|e| e.to_string())?; // 检查任务是否已存在 if downloads.contains_key(&task_id) { @@ -267,7 +316,7 @@ impl DownloadManager { // 获取任务信息,并在作用域结束时释放锁 let task_gid = { - let downloads = self.queue.lock().map_err(|e| e.to_string())?; + let downloads = self.download_queue.lock().map_err(|e| e.to_string())?; match downloads.get(&task_id) { Some(task) => task.gid.clone(), None => return Err(format!("找不到下载任务: {}", task_id)), @@ -296,7 +345,7 @@ impl DownloadManager { // 获取任务信息,并在作用域结束时释放锁 let task_gid = { - let downloads = self.queue.lock().map_err(|e| e.to_string())?; + let downloads = self.download_queue.lock().map_err(|e| e.to_string())?; match downloads.get(&task_id) { Some(task) => task.gid.clone(), None => return Err(format!("找不到下载任务: {}", task_id)), @@ -325,7 +374,7 @@ impl DownloadManager { // 获取任务信息,并在作用域结束时释放锁 let task_gid = { - let mut downloads = self.queue.lock().map_err(|e| e.to_string())?; + let mut downloads = self.download_queue.lock().map_err(|e| e.to_string())?; match downloads.get(&task_id) { Some(task) => { // 如果 aria2 未启动,只从队列中移除任务 @@ -348,7 +397,7 @@ impl DownloadManager { .map_err(|e| format!("取消下载任务失败: {}", e))?; // 从队列中移除任务 - let mut downloads = self.queue.lock().map_err(|e| e.to_string())?; + let mut downloads = self.download_queue.lock().map_err(|e| e.to_string())?; downloads.remove(&task_id); Ok(()) @@ -375,4 +424,82 @@ impl DownloadManager { self.aria2_started.store(false, Ordering::SeqCst); } } + + // 开始安装任务 + pub async fn process_install_queue(&self) -> Result<(), String> { + // 如果已经有任务在安装,直接返回 + if self.installing.load(Ordering::SeqCst) { + return Ok(()); + } + + // 查找第一个等待安装的任务 + 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() + .find(|(_, task)| matches!(task.status, InstallStatus::Queued)) + .map(|(id, task)| (id.clone(), task.clone())) + { + // 更新任务状态为安装中 + if let Some(task) = install_queue.get_mut(&id) { + task.status = InstallStatus::Installing; + } + (id, task) + } else { + return Ok(()); + } + }; + + // 标记为正在安装 + self.installing.store(true, Ordering::SeqCst); + + // 执行安装操作 + let install_result = self.install_package(&task).await; + + // 更新任务状态 + { + let mut install_queue = self.install_queue.lock().map_err(|e| e.to_string())?; + if let Some(task) = install_queue.get_mut(&task_id) { + task.status = match &install_result { + Ok(_) => InstallStatus::Installed, + Err(_) => InstallStatus::Error, + }; + } + } + + // 安装完成,重置安装状态 + self.installing.store(false, Ordering::SeqCst); + + // 如果安装失败,返回错误 + if let Err(e) = install_result { + return Err(format!("安装失败: {}", e)); + } + + Ok(()) + } + + // 实际执行安装的方法 + async fn install_package(&self, task: &InstallTask) -> Result<(), String> { + println!("开始安装包: {}", task.filepath); + + let output = Command::new("pkexec") + .arg("ssinstall") + .arg(&task.filepath) + .arg("--delete-after-install") + .output() + .map_err(|e| { + println!("安装命令执行失败: {}", e); + format!("执行安装命令失败: {}", e) + })?; + + if output.status.success() { + println!("安装成功完成"); + println!("命令输出: {}", String::from_utf8_lossy(&output.stdout)); + Ok(()) + } else { + let error_msg = String::from_utf8_lossy(&output.stderr).to_string(); + println!("安装失败: {}", error_msg); + println!("命令输出: {}", String::from_utf8_lossy(&output.stdout)); + Err(error_msg) + } + } } \ No newline at end of file diff --git a/src/components/DownloadCard/index.tsx b/src/components/DownloadCard/index.tsx index 9c3d749..8504d09 100644 --- a/src/components/DownloadCard/index.tsx +++ b/src/components/DownloadCard/index.tsx @@ -17,7 +17,11 @@ const DownloadCard: Component<{ download: DownloadTask }> = (props) => {
{props.download.status === 'queued' ? '排队中' : props.download.status === 'downloading' && props.download.speed ? - `${props.download.speed} - ` : ''} + `${props.download.speed} - ` : + props.download.status === 'error' ? '失败' : + props.download.status === 'installing' ? '正在安装' : + props.download.status === 'installed' ? '安装完成' : + ''} {props.download.status !== 'queued' && props.download.size}
diff --git a/src/features/downloads/store.ts b/src/features/downloads/store.ts index efc067e..53e536e 100644 --- a/src/features/downloads/store.ts +++ b/src/features/downloads/store.ts @@ -29,11 +29,11 @@ export const useDownloadsStore = () => { onCleanup(() => clearInterval(intervalId)); const activeDownloads = () => downloads().filter(item => - ['downloading', 'paused', 'queued'].includes(item.status) + ['downloading', 'paused', 'queued', 'completed', 'installing'].includes(item.status) ); const completedDownloads = () => downloads().filter(item => - item.status === 'completed' + ['installed', 'error'].includes(item.status) ); const addDownload = async (category: string, pkgname: string, filename: string, name: string) => { diff --git a/src/types/download.ts b/src/types/download.ts index 9d6dbe8..7139ccd 100644 --- a/src/types/download.ts +++ b/src/types/download.ts @@ -4,7 +4,9 @@ export enum DownloadStatus { queued = 'queued', paused = 'paused', completed = 'completed', - error = 'error' + error = 'error', + installing = 'installing', + installed = 'installed' } // 下载任务接口