From c8d8339f307d01fdb0a9ddd6338082fd89157133 Mon Sep 17 00:00:00 2001 From: purp1e Date: Sat, 8 Nov 2025 18:09:35 +0800 Subject: [PATCH] [feat] more hw info and update feature --- src-tauri/src/cmds.rs | 72 ++- src-tauri/src/main.rs | 1 + src-tauri/src/tool/updater.rs | 601 ++++++++++-------- src-tauri/tauri.conf.json | 2 +- src/app/(main)/preference/general/page.tsx | 65 +- src/components/cstb/FpsTest.tsx | 183 +++--- .../FpsTest/components/ResolutionConfig.tsx | 8 +- .../FpsTest/components/TestConfigPanel.tsx | 18 - .../FpsTest/components/TestResultsTable.tsx | 25 +- .../cstb/FpsTest/services/resultReader.ts | 16 + .../cstb/FpsTest/utils/csv-export.ts | 44 ++ src/components/cstb/UpdateChecker.tsx | 264 +++++--- src/store/app.ts | 14 +- 13 files changed, 813 insertions(+), 500 deletions(-) diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs index 2c472b5..0b66ded 100644 --- a/src-tauri/src/cmds.rs +++ b/src-tauri/src/cmds.rs @@ -9,6 +9,8 @@ use std::fs::File; use std::fs; use std::path::Path; use std::io::{BufRead, BufReader}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, OnceLock}; use tauri::path::BaseDirectory; use tauri::Manager; use serde::{Deserialize, Serialize}; @@ -17,6 +19,13 @@ use serde::{Deserialize, Serialize}; // pub type Result = Result; +// 全局下载取消标志 +static DOWNLOAD_CANCELLED: OnceLock> = OnceLock::new(); + +fn get_download_cancelled() -> Arc { + DOWNLOAD_CANCELLED.get_or_init(|| Arc::new(AtomicBool::new(false))).clone() +} + #[tauri::command] pub fn greet(name: &str) -> Result { Ok(format!("Hello, {}! You've been greeted from Rust!", name)) @@ -301,20 +310,46 @@ pub fn read_vprof_report(console_log_path: &str) -> Result { // 更新相关命令 -/// 检查更新 +/// 检查更新(支持 GitHub Release 和自定义端点) #[tauri::command] pub async fn check_app_update( app: tauri::AppHandle, - custom_endpoint: Option, - github_repo: Option, + endpoint: Option, + use_mirror: Option, + include_prerelease: Option, ) -> Result, String> { let current_version = app.package_info().version.to_string(); + let use_mirror = use_mirror.unwrap_or(false); + let include_prerelease = include_prerelease.unwrap_or(false); - wrap_err!(check_update( - custom_endpoint.as_deref(), - github_repo.as_deref(), - ¤t_version - ).await) + println!("[检查更新命令] 当前应用版本: {}", current_version); + println!("[检查更新命令] 使用镜像: {}", use_mirror); + println!("[检查更新命令] 包含预发布版本: {}", include_prerelease); + if let Some(ref ep) = endpoint { + println!("[检查更新命令] 自定义端点: {}", ep); + } + + // 从环境变量获取 GitHub 仓库信息,如果没有则使用默认值 + const DEFAULT_GITHUB_REPO: &str = "plsgo/cstb"; + let github_repo_str = std::env::var("GITHUB_REPO").ok(); + let github_repo = github_repo_str.as_deref().unwrap_or(DEFAULT_GITHUB_REPO); + println!("[检查更新命令] GitHub 仓库: {}", github_repo); + + let result = wrap_err!(check_update( + endpoint.as_deref(), + ¤t_version, + use_mirror, + Some(github_repo), + include_prerelease + ).await)?; + + if result.is_some() { + println!("[检查更新命令] ✓ 返回更新信息"); + } else { + println!("[检查更新命令] ✗ 无更新可用"); + } + + Ok(result) } /// 下载更新 @@ -323,15 +358,22 @@ pub async fn download_app_update( app: tauri::AppHandle, download_url: String, ) -> Result { - let path = wrap_err!(download_update( - &app, - &download_url, - None // 可以添加进度回调 - ).await)?; + // 重置取消标志 + let cancelled = get_download_cancelled(); + cancelled.store(false, Ordering::Relaxed); + let path = wrap_err!(download_update(&app, &download_url, cancelled).await)?; Ok(path.to_string_lossy().to_string()) } +/// 取消下载 +#[tauri::command] +pub fn cancel_download_update() -> Result<(), String> { + let cancelled = get_download_cancelled(); + cancelled.store(true, Ordering::Relaxed); + Ok(()) +} + /// 安装更新 #[tauri::command] pub fn install_app_update(installer_path: String) -> Result<(), String> { @@ -638,7 +680,7 @@ pub async fn get_monitor_info() -> Result, String> { } // 尝试获取刷新率和分辨率信息 - let display_output = Command::new("powershell") + let _display_output = Command::new("powershell") .args(&[ "-NoProfile", "-Command", @@ -648,7 +690,7 @@ pub async fn get_monitor_info() -> Result, String> { .await; // 获取刷新率信息 - let refresh_output = Command::new("powershell") + let _refresh_output = Command::new("powershell") .args(&[ "-NoProfile", "-Command", diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 2e91bcc..62525f4 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -168,6 +168,7 @@ fn main() { cmds::read_vprof_report, cmds::check_app_update, cmds::download_app_update, + cmds::cancel_download_update, cmds::install_app_update, cmds::get_computer_info, cmds::get_gpu_info, diff --git a/src-tauri/src/tool/updater.rs b/src-tauri/src/tool/updater.rs index 3672566..8652e5d 100644 --- a/src-tauri/src/tool/updater.rs +++ b/src-tauri/src/tool/updater.rs @@ -3,8 +3,10 @@ use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; use std::process::Command; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use tauri::{Emitter, Manager}; use tauri::path::BaseDirectory; -use tauri::Manager; #[cfg(windows)] use std::os::windows::process::CommandExt; @@ -17,303 +19,360 @@ const CREATE_NO_WINDOW: u32 = 0x08000000; pub struct UpdateInfo { pub version: String, pub notes: Option, - pub pub_date: Option, pub download_url: String, - pub signature: Option, } -/// 自定义更新服务器响应格式 -#[derive(Debug, Deserialize)] -struct CustomUpdateResponse { +/// gh-info API 响应结构 +#[derive(Debug, Clone, Serialize, Deserialize)] +struct GhInfoApiResponse { + repo: String, + latest_version: String, + changelog: Option, + published_at: String, + #[serde(default)] + prerelease: bool, + attachments: serde_json::Value, // 支持两种格式: ["URL1", "URL2"] 或 [["文件名", "URL"], ...] +} + +/// 自定义更新服务器 API 响应结构 +#[derive(Debug, Clone, Serialize, Deserialize)] +struct CustomUpdateApiResponse { version: String, notes: Option, + #[serde(rename = "pub_date")] pub_date: Option, download_url: String, signature: Option, - platforms: Option, + platforms: Option>, } -/// 平台特定的下载链接 -#[derive(Debug, Deserialize)] -struct PlatformDownloads { - #[serde(rename = "windows-x86_64")] - windows_x86_64: Option, - #[serde(rename = "darwin-x86_64")] - darwin_x86_64: Option, - #[serde(rename = "darwin-aarch64")] - darwin_aarch64: Option, - #[serde(rename = "linux-x86_64")] - linux_x86_64: Option, -} - -#[derive(Debug, Deserialize)] +/// 平台特定信息 +#[derive(Debug, Clone, Serialize, Deserialize)] struct PlatformInfo { url: String, signature: Option, } -/// GitHub Release API 响应 -#[derive(Debug, Deserialize)] -struct GitHubRelease { - tag_name: String, - name: Option, - body: Option, - published_at: Option, - assets: Vec, -} - -#[derive(Debug, Deserialize)] -struct GitHubAsset { - name: String, - browser_download_url: String, - content_type: String, -} - -/// 检查更新 -/// -/// # 参数 -/// - `custom_endpoint`: 自定义更新服务器端点 URL(可选) -/// - `github_repo`: GitHub 仓库(格式:owner/repo,可选) -/// - `current_version`: 当前应用版本 -/// -/// # 返回 -/// 如果有更新,返回 UpdateInfo;否则返回 None +/// 检查更新(使用自定义 API 端点) pub async fn check_update( - custom_endpoint: Option<&str>, - github_repo: Option<&str>, + endpoint: Option<&str>, current_version: &str, + _use_mirror: bool, + github_repo: Option<&str>, + include_prerelease: bool, ) -> Result> { - // 首先尝试自定义服务器 - if let Some(endpoint) = custom_endpoint { - if !endpoint.is_empty() { - match check_custom_update(endpoint).await { - Ok(Some(info)) => { - if compare_versions(&info.version, current_version) > 0 { - return Ok(Some(info)); - } - } - Err(e) => { - log::warn!("自定义更新服务器检查失败: {}", e); - } - Ok(None) => {} - } + println!("[更新检查] 开始检查更新..."); + println!("[更新检查] 当前版本: {}", current_version); + println!("[更新检查] 包含预发布版本: {}", include_prerelease); + + // 确定使用的 API 端点 + let api_url = if let Some(custom_endpoint) = endpoint { + // 如果提供了自定义端点,直接使用 + custom_endpoint.to_string() + } else { + // 否则使用默认的 gh-info API + let repo = github_repo.unwrap_or("plsgo/cstb"); + if include_prerelease { + format!("https://gh-info.okk.cool/repos/{}/releases/latest/pre", repo) + } else { + format!("https://gh-info.okk.cool/repos/{}/releases/latest", repo) } - } + }; + + println!("[更新检查] API URL: {}", api_url); - // Fallback 到 GitHub Release - if let Some(repo) = github_repo { - match check_github_update(repo).await { - Ok(Some(info)) => { - if compare_versions(&info.version, current_version) > 0 { - return Ok(Some(info)); - } - } - Err(e) => { - log::warn!("GitHub Release 检查失败: {}", e); - } - Ok(None) => {} - } - } - - Ok(None) -} - -/// 检查自定义更新服务器 -async fn check_custom_update(endpoint: &str) -> Result> { let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(10)) + .user_agent("cstb-updater/1.0") .build()?; - let response = client.get(endpoint).send().await?; + let response = client.get(&api_url).send().await?; if !response.status().is_success() { - return Err(anyhow::anyhow!("HTTP 状态码: {}", response.status())); + println!("[更新检查] ✗ API 请求失败,HTTP 状态码: {}", response.status()); + return Err(anyhow::anyhow!("API 请求失败,HTTP 状态码: {}", response.status())); } - let text = response.text().await?; - let update_resp: CustomUpdateResponse = serde_json::from_str(&text) - .context("解析自定义更新服务器响应失败")?; + // 获取响应文本以便尝试不同的解析方式 + let response_text = response.text().await?; + // 关闭更新日志的打印 + // println!("[更新检查] API 响应: {}", response_text); - // 获取平台特定的下载链接 - let download_url = if let Some(platforms) = update_resp.platforms { - #[cfg(target_os = "windows")] - { - #[cfg(target_arch = "x86_64")] - { - platforms - .windows_x86_64 - .map(|p| p.url) - .unwrap_or(update_resp.download_url) - } - } - #[cfg(target_os = "macos")] - { - #[cfg(target_arch = "x86_64")] - { - platforms - .darwin_x86_64 - .map(|p| p.url) - .unwrap_or(update_resp.download_url) - } - #[cfg(target_arch = "aarch64")] - { - platforms - .darwin_aarch64 - .map(|p| p.url) - .unwrap_or(update_resp.download_url) - } - } - #[cfg(target_os = "linux")] - { - #[cfg(target_arch = "x86_64")] - { - platforms - .linux_x86_64 - .map(|p| p.url) - .unwrap_or(update_resp.download_url) - } + // 尝试解析为自定义更新服务器格式 + let update_info = if let Ok(custom_resp) = serde_json::from_str::(&response_text) { + println!("[更新检查] 检测到自定义更新服务器格式"); + + // 提取版本号(去掉 'v' 前缀) + let version = custom_resp.version.trim_start_matches('v').to_string(); + println!("[更新检查] 远程版本: {}", version); + + // 版本比较 + let comparison = compare_version(&version, current_version); + println!("[更新检查] 版本比较结果: {} (1=有新版本, 0=相同, -1=当前更新)", comparison); + + if comparison > 0 { + println!("[更新检查] ✓ 发现新版本: {}", version); + + // 获取下载链接 + // 优先使用平台特定的链接 + let download_url = if let Some(ref platforms) = custom_resp.platforms { + // 检测当前平台 + #[cfg(target_os = "windows")] + let platform_key = "windows-x86_64"; + #[cfg(all(target_os = "macos", target_arch = "x86_64"))] + let platform_key = "darwin-x86_64"; + #[cfg(all(target_os = "macos", target_arch = "aarch64"))] + let platform_key = "darwin-aarch64"; + #[cfg(target_os = "linux")] + let platform_key = "linux-x86_64"; + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + let platform_key = ""; + + if !platform_key.is_empty() { + platforms.get(platform_key) + .map(|p| p.url.clone()) + .unwrap_or_else(|| custom_resp.download_url.clone()) + } else { + custom_resp.download_url.clone() + } + } else { + custom_resp.download_url.clone() + }; + + println!("[更新检查] 下载链接: {}", download_url); + + Some(UpdateInfo { + version, + notes: custom_resp.notes, + download_url, + }) + } else { + println!("[更新检查] ✗ 已是最新版本"); + None } } else { - update_resp.download_url + // 尝试解析为 gh-info API 格式 + println!("[更新检查] 尝试解析为 gh-info API 格式"); + let api_resp: GhInfoApiResponse = serde_json::from_str(&response_text) + .context("解析更新 API 响应失败,既不是自定义格式也不是 gh-info 格式")?; + + // 提取版本号(去掉 'v' 前缀) + let version = api_resp.latest_version.trim_start_matches('v').to_string(); + println!("[更新检查] 远程版本: {}", version); + + // 版本比较 + let comparison = compare_version(&version, current_version); + println!("[更新检查] 版本比较结果: {} (1=有新版本, 0=相同, -1=当前更新)", comparison); + + if comparison > 0 { + println!("[更新检查] ✓ 发现新版本: {}", version); + + // 从 attachments 中获取下载链接 + // 支持两种格式: + // 1. 字符串数组: ["URL1", "URL2", ...] + // 2. 嵌套数组: [["文件名", "URL"], ...] + let download_url = extract_download_url(&api_resp.attachments) + .ok_or_else(|| anyhow::anyhow!("未找到可下载的安装包"))?; + + println!("[更新检查] 下载链接: {}", download_url); + + Some(UpdateInfo { + version, + notes: api_resp.changelog, + download_url, + }) + } else { + println!("[更新检查] ✗ 已是最新版本"); + None + } }; - Ok(Some(UpdateInfo { - version: update_resp.version, - notes: update_resp.notes, - pub_date: update_resp.pub_date, - download_url, - signature: update_resp.signature, - })) + Ok(update_info) } -/// 检查 GitHub Release -async fn check_github_update(repo: &str) -> Result> { - let url = format!("https://api.github.com/repos/{}/releases/latest", repo); +/// 从 attachments 中提取下载 URL +/// 支持两种格式: +/// 1. 字符串数组: ["URL1", "URL2", ...] - 优先选择 .exe 或 .msi 文件 +/// 2. 嵌套数组: [["文件名", "URL"], ...] - 优先选择 .exe 或 .msi 文件 +fn extract_download_url(attachments: &serde_json::Value) -> Option { + // 尝试解析为字符串数组格式: ["URL1", "URL2", ...] + if let Ok(urls) = serde_json::from_value::>(attachments.clone()) { + println!("[更新检查] 检测到字符串数组格式的 attachments"); + // 优先选择 .exe 或 .msi 文件 + if let Some(url) = urls.iter().find(|url| { + url.ends_with(".exe") || url.ends_with(".msi") + }) { + return Some(url.clone()); + } + // 如果没有找到 .exe 或 .msi,使用第一个 URL + return urls.first().cloned(); + } - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(10)) - .user_agent("CS工具箱/1.0") - .build()?; - - let response = client.get(&url).send().await?; - - if !response.status().is_success() { - return Err(anyhow::anyhow!("HTTP 状态码: {}", response.status())); + // 尝试解析为嵌套数组格式: [["文件名", "URL"], ...] + if let Ok(nested) = serde_json::from_value::>>(attachments.clone()) { + println!("[更新检查] 检测到嵌套数组格式的 attachments"); + // 优先选择 .exe 或 .msi 文件 + if let Some(url) = nested.iter().find_map(|attachment| { + if attachment.len() >= 2 { + let filename = &attachment[0]; + let url = &attachment[1]; + if filename.ends_with(".exe") || filename.ends_with(".msi") { + Some(url.clone()) + } else { + None + } + } else { + None + } + }) { + return Some(url); + } + // 如果没有找到 .exe 或 .msi,使用第一个附件的 URL + if let Some(attachment) = nested.first() { + if attachment.len() >= 2 { + return Some(attachment[1].clone()); + } + } } - - let release: GitHubRelease = response.json().await?; - - // 从 tag_name 中提取版本号(移除可能的 'v' 前缀) - let version = release.tag_name.trim_start_matches('v').to_string(); - - // 查找适合当前平台的安装包 - let download_url = find_platform_asset(&release.assets)?; - - Ok(Some(UpdateInfo { - version, - notes: release.body, - pub_date: release.published_at, - download_url, - signature: None, // GitHub Release 通常不包含签名 - })) + + None } -/// 查找适合当前平台的资源文件 -fn find_platform_asset(assets: &[GitHubAsset]) -> Result { - #[cfg(target_os = "windows")] - { - // 查找 .exe 或 .msi 或 .nsis 安装包 - for asset in assets { - let name = asset.name.to_lowercase(); - if name.ends_with(".exe") - || name.ends_with(".msi") - || (name.contains("windows") && name.contains("x86_64")) - { - return Ok(asset.browser_download_url.clone()); - } - } +/// 改进的版本比较函数,支持预发布版本(beta.5, beta.6等) +fn compare_version(new: &str, current: &str) -> i32 { + println!("[版本比较] 比较版本: '{}' vs '{}'", new, current); + + // 解析版本号:支持格式如 "0.0.6-beta.5", "beta.6", "0.0.6" 等 + let (new_base, new_pre) = parse_version(new); + let (current_base, current_pre) = parse_version(current); + + println!("[版本比较] 新版本 - 基础部分: {:?}, 预发布部分: {:?}", new_base, new_pre); + println!("[版本比较] 当前版本 - 基础部分: {:?}, 预发布部分: {:?}", current_base, current_pre); + + // 先比较基础版本号(数字部分) + let base_comparison = compare_version_parts(&new_base, ¤t_base); + + if base_comparison != 0 { + println!("[版本比较] 基础版本不同,返回: {}", base_comparison); + return base_comparison; } - - #[cfg(target_os = "macos")] - { - #[cfg(target_arch = "x86_64")] - { - for asset in assets { - let name = asset.name.to_lowercase(); - if name.ends_with(".dmg") { - if name.contains("x86_64") || name.contains("darwin-x86_64") || (!name.contains("aarch64") && !name.contains("arm64")) { - return Ok(asset.browser_download_url.clone()); - } - } - } - } - #[cfg(target_arch = "aarch64")] - { - for asset in assets { - let name = asset.name.to_lowercase(); - if name.ends_with(".dmg") { - if name.contains("aarch64") || name.contains("darwin-aarch64") || name.contains("arm64") || (!name.contains("x86_64") && !name.contains("intel")) { - return Ok(asset.browser_download_url.clone()); - } - } - } - } + + // 如果基础版本相同(或都为空),比较预发布标识符 + // 如果基础版本都为空,说明是纯预发布版本(如 beta.5 vs beta.6) + let pre_comparison = compare_prerelease(&new_pre, ¤t_pre); + println!("[版本比较] 预发布版本比较结果: {}", pre_comparison); + + // 如果基础版本都为空且预发布比较结果为0,说明版本完全相同 + if new_base.is_empty() && current_base.is_empty() && pre_comparison == 0 { + return 0; } - - #[cfg(target_os = "linux")] - { - for asset in assets { - let name = asset.name.to_lowercase(); - if name.ends_with(".deb") - || name.ends_with(".rpm") - || name.ends_with(".appimage") - || (name.contains("linux") && name.contains("x86_64")) - { - return Ok(asset.browser_download_url.clone()); - } - } - } - - // 如果找不到特定平台的,返回第一个资源 - if let Some(asset) = assets.first() { - return Ok(asset.browser_download_url.clone()); - } - - Err(anyhow::anyhow!("未找到适合当前平台的安装包")) + + pre_comparison } -/// 比较版本号 -/// 返回: 1 表示 version1 > version2, -1 表示 version1 < version2, 0 表示相等 -fn compare_versions(version1: &str, version2: &str) -> i32 { - let v1_parts: Vec<&str> = version1 - .split(|c: char| c == '.' || c == '-' || c == '_') - .collect(); - let v2_parts: Vec<&str> = version2 - .split(|c: char| c == '.' || c == '-' || c == '_') +/// 解析版本号,返回(基础版本号数组,预发布标识符) +fn parse_version(version: &str) -> (Vec, Option) { + // 去掉 'v' 前缀 + let version = version.trim_start_matches('v').trim(); + + // 检查是否有预发布标识符(如 -beta.5, -alpha.1 等) + let (base_str, pre_str) = if let Some(dash_pos) = version.find('-') { + let (base, pre) = version.split_at(dash_pos); + (base, Some(pre[1..].to_string())) // 跳过 '-' 字符 + } else { + (version, None) + }; + + // 解析基础版本号(数字部分) + let base_parts: Vec = base_str + .split('.') + .filter_map(|s| s.parse().ok()) .collect(); + + // 如果基础版本号为空且没有预发布标识符,可能是纯预发布版本(如 "beta.5") + // 这种情况下,整个字符串作为预发布标识符 + if base_parts.is_empty() && pre_str.is_none() { + // 检查是否包含非数字字符(可能是预发布版本) + if !version.chars().any(|c| c.is_ascii_digit()) { + return (vec![], Some(version.to_string())); + } + } + + (base_parts, pre_str) +} - let max_len = v1_parts.len().max(v2_parts.len()); - +/// 比较版本号数组(数字部分) +fn compare_version_parts(new: &[u32], current: &[u32]) -> i32 { + let max_len = new.len().max(current.len()); + for i in 0..max_len { - let v1_part = v1_parts.get(i).and_then(|s| s.parse::().ok()).unwrap_or(0); - let v2_part = v2_parts.get(i).and_then(|s| s.parse::().ok()).unwrap_or(0); - - if v1_part > v2_part { + let new_val = new.get(i).copied().unwrap_or(0); + let current_val = current.get(i).copied().unwrap_or(0); + + if new_val > current_val { return 1; - } else if v1_part < v2_part { + } else if new_val < current_val { return -1; } } - + 0 } -/// 下载更新文件 +/// 比较预发布标识符 +/// 规则: +/// - 有预发布标识符的版本 < 没有预发布标识符的版本 +/// - 如果都有预发布标识符,按字典序比较 +fn compare_prerelease(new: &Option, current: &Option) -> i32 { + match (new, current) { + // 都没有预发布标识符,版本相同 + (None, None) => 0, + // 新版本有预发布,当前版本没有 -> 新版本更旧(预发布版本 < 正式版本) + (Some(_), None) => -1, + // 新版本没有预发布,当前版本有 -> 新版本更新 + (None, Some(_)) => 1, + // 都有预发布标识符,按字典序比较 + (Some(new_pre), Some(current_pre)) => { + // 尝试提取数字部分进行比较(如 beta.5 -> 5, beta.6 -> 6) + let new_num = extract_number_from_prerelease(new_pre); + let current_num = extract_number_from_prerelease(current_pre); + + if let (Some(new_n), Some(current_n)) = (new_num, current_num) { + // 如果都能提取数字,比较数字 + if new_n > current_n { + 1 + } else if new_n < current_n { + -1 + } else { + // 数字相同,按字符串比较 + new_pre.cmp(current_pre) as i32 + } + } else { + // 无法提取数字,按字符串比较 + new_pre.cmp(current_pre) as i32 + } + } + } +} + +/// 从预发布标识符中提取数字(如 "beta.5" -> 5, "alpha.1" -> 1) +fn extract_number_from_prerelease(pre: &str) -> Option { + // 尝试从最后一部分提取数字 + if let Some(last_part) = pre.split('.').last() { + last_part.parse().ok() + } else { + None + } +} + +/// 下载更新文件(带进度追踪和取消支持) pub async fn download_update( app: &tauri::AppHandle, download_url: &str, - progress_callback: Option>, + cancelled: Arc, ) -> Result { + println!("[下载更新] 开始下载,下载链接: {}", download_url); + let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(300)) .build()?; @@ -324,6 +383,7 @@ pub async fn download_update( return Err(anyhow::anyhow!("下载失败,HTTP 状态码: {}", response.status())); } + // 获取文件总大小 let total_size = response.content_length().unwrap_or(0); // 获取缓存目录 @@ -344,6 +404,10 @@ pub async fn download_update( .unwrap_or("update"); let file_path = cache_dir.join(filename); + + println!("[下载更新] 文件名: {}", filename); + println!("[下载更新] 文件大小: {} bytes ({:.2} MB)", total_size, total_size as f64 / 1024.0 / 1024.0); + println!("[下载更新] 保存路径: {}", file_path.display()); // 下载文件 let mut file = fs::File::create(&file_path)?; @@ -354,76 +418,65 @@ pub async fn download_update( use std::io::Write; while let Some(item) = stream.next().await { + // 检查是否取消 + if cancelled.load(Ordering::Relaxed) { + // 删除部分下载的文件 + let _ = fs::remove_file(&file_path); + return Err(anyhow::anyhow!("下载已取消")); + } + let chunk = item?; file.write_all(&chunk)?; downloaded += chunk.len() as u64; - if let Some(ref callback) = progress_callback { - callback(downloaded, total_size); + // 发送进度事件 + if total_size > 0 { + let progress = (downloaded * 100) / total_size; + let _ = app.emit("update-download-progress", progress); + } else { + // 如果无法获取总大小,发送已下载的字节数 + let _ = app.emit("update-download-progress", downloaded); } } file.sync_all()?; + // 发送完成事件 + let _ = app.emit("update-download-progress", 100u64); + + println!("[下载更新] ✓ 下载完成,文件已保存到: {}", file_path.display()); + Ok(file_path) } -/// 安装更新(Windows NSIS) +/// 安装更新(Windows) #[cfg(target_os = "windows")] pub fn install_update(installer_path: &str) -> Result<()> { - // 使用静默安装参数 let mut cmd = Command::new(installer_path); - cmd.args(&["/S", "/D=C:\\Program Files\\CS工具箱"]); - + cmd.args(&["/S"]); // 静默安装 cmd.creation_flags(CREATE_NO_WINDOW); cmd.spawn()?; - Ok(()) } /// 安装更新(macOS) #[cfg(target_os = "macos")] pub fn install_update(installer_path: &str) -> Result<()> { - // macOS 通常需要用户手动安装 DMG - // 这里打开安装包 - Command::new("open") - .arg(installer_path) - .spawn()?; - + Command::new("open").arg(installer_path).spawn()?; Ok(()) } /// 安装更新(Linux) #[cfg(target_os = "linux")] pub fn install_update(installer_path: &str) -> Result<()> { - // Linux 根据文件类型选择安装方式 if installer_path.ends_with(".deb") { Command::new("sudo") .args(&["dpkg", "-i", installer_path]) .spawn()?; - } else if installer_path.ends_with(".rpm") { - Command::new("sudo") - .args(&["rpm", "-i", installer_path]) - .spawn()?; } else if installer_path.ends_with(".AppImage") { - // AppImage 通常只需要设置执行权限 Command::new("chmod") .args(&["+x", installer_path]) .spawn()?; } - Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_compare_versions() { - assert_eq!(compare_versions("1.0.1", "1.0.0"), 1); - assert_eq!(compare_versions("1.0.0", "1.0.1"), -1); - assert_eq!(compare_versions("1.0.0", "1.0.0"), 0); - assert_eq!(compare_versions("0.0.6-beta.4", "0.0.6"), 0); // 简单比较,不考虑 beta - } -} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f76c468..a1989f4 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -51,7 +51,7 @@ }, "productName": "CS工具箱", "mainBinaryName": "cstb", - "version": "0.0.6-beta.6", + "version": "0.0.6-beta.5", "identifier": "upup.cool", "plugins": { "deep-link": { diff --git a/src/app/(main)/preference/general/page.tsx b/src/app/(main)/preference/general/page.tsx index 85150ee..80e341d 100644 --- a/src/app/(main)/preference/general/page.tsx +++ b/src/app/(main)/preference/general/page.tsx @@ -1,29 +1,76 @@ "use client" +import { useEffect } from "react" import { useAppStore } from "@/store/app" -import { Switch } from "@heroui/react" +import { Switch, Chip } from "@heroui/react" import { UpdateChecker } from "@/components/cstb/UpdateChecker" +import { getVersion } from "@tauri-apps/api/app" export default function Page() { const app = useAppStore() + // 初始化版本号(如果还没有设置) + useEffect(() => { + if (typeof window !== "undefined" && (!app.state.version || app.state.version === "0.0.1")) { + void getVersion().then((version) => { + app.setVersion(version) + }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + // 从环境变量或配置中获取更新服务器地址 - // 这里可以改为从 store 或配置文件中读取 const customEndpoint = process.env.NEXT_PUBLIC_UPDATE_ENDPOINT || "" - const githubRepo = process.env.NEXT_PUBLIC_GITHUB_REPO || "" return (
-

版本号:{app.state.version}

-

是否有更新:{app.state.hasUpdate ? "有" : "无"}

-

是否使用镜像源:{app.state.useMirror ? "是" : "否"}

+
+

版本号:{app.state.version}

+ {app.state.hasUpdate && app.state.latestVersion && ( + + {app.state.latestVersion} + + )} +
+ {/*

是否有更新:{app.state.hasUpdate ? "有" : "无"}

*/} + {/*

是否使用镜像源:{app.state.useMirror ? "是" : "否"}

*/}
- {/*
+

更新检查

- -
*/} +
+ {/* app.setUseMirror(e.target.checked)} + > + 使用镜像源 + */} + {/*

+ {app.state.useMirror + ? "使用自建更新服务检查更新" + : "使用 GitHub Release 检查更新"} +

*/} + app.setIncludePrerelease(e.target.checked)} + > + 包含测试版 + +

+ {app.state.includePrerelease + ? "检查更新时会包含预发布版本(beta、alpha等)" + : "仅检查正式版本"} +

+
+ +

启动设置

diff --git a/src/components/cstb/FpsTest.tsx b/src/components/cstb/FpsTest.tsx index 09c377a..e022f01 100644 --- a/src/components/cstb/FpsTest.tsx +++ b/src/components/cstb/FpsTest.tsx @@ -19,22 +19,21 @@ import { Input, } from "@heroui/react" import { useState, useEffect, useRef, useCallback } from "react" -import { - TestTube, - Power, - Play, - Check, - Close, - Square, - DownloadOne, - List, -} from "@icon-park/react" +import { TestTube, Power, Play, Check, Close, Square, DownloadOne, List } from "@icon-park/react" // 导入提取的模块 import { BENCHMARK_MAPS, TEST_TIMEOUT, PRESET_RESOLUTIONS } from "./FpsTest/constants" import { parseVProfReport } from "./FpsTest/utils/vprof-parser" -import { compareTimestamps, formatCurrentTimestamp, timestampToISO } from "./FpsTest/utils/timestamp" +import { + compareTimestamps, + formatCurrentTimestamp, + timestampToISO, +} from "./FpsTest/utils/timestamp" import { extractFpsMetrics } from "./FpsTest/utils/fps-metrics" -import { handleExportCSV, handleExportAverageCSV, formatVideoSettingSummary } from "./FpsTest/utils/csv-export" +import { + handleExportCSV, + handleExportAverageCSV, + formatVideoSettingSummary, +} from "./FpsTest/utils/csv-export" import { useGameMonitor } from "./FpsTest/hooks/useGameMonitor" import { useHardwareInfo } from "./FpsTest/hooks/useHardwareInfo" import { NoteCell } from "./FpsTest/components/NoteCell" @@ -94,9 +93,17 @@ export function FpsTest() { // 记录测试开始时的视频设置 const testStartVideoSettingRef = useRef(null) // 记录当前测试的分辨率信息(用于备注) - const currentTestResolutionRef = useRef<{ width: string; height: string; label: string } | null>(null) + const currentTestResolutionRef = useRef<{ width: string; height: string; label: string } | null>( + null + ) // 记录当前分辨率在分辨率组中的索引和总测试次数(用于批量测试备注) - const currentResolutionGroupInfoRef = useRef<{ resIndex: number; totalResolutions: number; totalTestCount: number; currentBatchIndex: number; batchCount: number } | null>(null) + const currentResolutionGroupInfoRef = useRef<{ + resIndex: number + totalResolutions: number + totalTestCount: number + currentBatchIndex: number + batchCount: number + } | null>(null) // 记录最后一次测试的时间戳(用于平均值记录) const lastTestTimestampRef = useRef(null) @@ -176,27 +183,31 @@ export function FpsTest() { memory: hardwareInfo.systemInfo.total_memory ? Math.round(hardwareInfo.systemInfo.total_memory / 1024 / 1024 / 1024) : null, - memoryManufacturer: hardwareInfo.memoryInfo && hardwareInfo.memoryInfo.length > 0 - ? hardwareInfo.memoryInfo[0].manufacturer || null - : null, - memorySpeed: hardwareInfo.memoryInfo && hardwareInfo.memoryInfo.length > 0 - ? hardwareInfo.memoryInfo[0].speed || null - : null, - memoryDefaultSpeed: hardwareInfo.memoryInfo && hardwareInfo.memoryInfo.length > 0 - ? hardwareInfo.memoryInfo[0].default_speed || null - : null, - gpu: hardwareInfo.gpuInfo - ? hardwareInfo.gpuInfo.model - : null, - monitor: hardwareInfo.monitorInfo && hardwareInfo.monitorInfo.length > 0 - ? hardwareInfo.monitorInfo[0].name || null - : null, - monitorManufacturer: hardwareInfo.monitorInfo && hardwareInfo.monitorInfo.length > 0 - ? hardwareInfo.monitorInfo[0].manufacturer || null - : null, - monitorModel: hardwareInfo.monitorInfo && hardwareInfo.monitorInfo.length > 0 - ? hardwareInfo.monitorInfo[0].model || null - : null, + memoryManufacturer: + hardwareInfo.memoryInfo && hardwareInfo.memoryInfo.length > 0 + ? hardwareInfo.memoryInfo[0].manufacturer || null + : null, + memorySpeed: + hardwareInfo.memoryInfo && hardwareInfo.memoryInfo.length > 0 + ? hardwareInfo.memoryInfo[0].speed || null + : null, + memoryDefaultSpeed: + hardwareInfo.memoryInfo && hardwareInfo.memoryInfo.length > 0 + ? hardwareInfo.memoryInfo[0].default_speed || null + : null, + gpu: hardwareInfo.gpuInfo ? hardwareInfo.gpuInfo.model : null, + monitor: + hardwareInfo.monitorInfo && hardwareInfo.monitorInfo.length > 0 + ? hardwareInfo.monitorInfo[0].name || null + : null, + monitorManufacturer: + hardwareInfo.monitorInfo && hardwareInfo.monitorInfo.length > 0 + ? hardwareInfo.monitorInfo[0].manufacturer || null + : null, + monitorModel: + hardwareInfo.monitorInfo && hardwareInfo.monitorInfo.length > 0 + ? hardwareInfo.monitorInfo[0].model || null + : null, motherboardModel: hardwareInfo.motherboardInfo?.model || null, motherboardVersion: hardwareInfo.motherboardInfo?.version || null, biosVersion: hardwareInfo.computerInfo?.BiosSMBIOSBIOSVersion || null, @@ -223,9 +234,9 @@ export function FpsTest() { } catch (error) { console.error("获取控制台日志路径失败:", error) if (!silent) { - addToast({ - title: "获取控制台日志路径失败", - color: "warning" + addToast({ + title: "获取控制台日志路径失败", + color: "warning", }) } return false @@ -240,9 +251,9 @@ export function FpsTest() { } catch (error) { console.error("读取性能报告失败:", error) if (!silent) { - addToast({ - title: "读取性能报告失败", - color: "warning" + addToast({ + title: "读取性能报告失败", + color: "warning", }) } return false @@ -281,11 +292,6 @@ export function FpsTest() { timeoutRef.current = null } - // 测试结束后读取视频设置(检测分辨率) - if (steam.state.steamDirValid && steam.currentUser()) { - await tool.getVideoConfig(steam.state.steamDir, steam.currentUser()?.steam_id32 || 0) - } - // 提取 avg 和 p1 值 const { avg, p1 } = extractFpsMetrics(parsed.data) @@ -294,8 +300,13 @@ export function FpsTest() { const testDate = now.toISOString() const mapConfig = BENCHMARK_MAPS[selectedMapIndex] - // 使用读取到的视频设置(测试结束后读取的) - const currentVideoSetting = tool.store.state.videoSetting + // 测试结束时读取视频设置(检测分辨率) + // 无论是自动监听还是手动读取,都在测试结束时读取当前的视频设置 + let currentVideoSetting: typeof tool.state.videoSetting | null = null + if (steam.state.steamDirValid && steam.currentUser()) { + await tool.getVideoConfig(steam.state.steamDir, steam.currentUser()?.steam_id32 || 0) + currentVideoSetting = tool.store.state.videoSetting + } // 如果是批量测试,保存结果到批量结果数组,否则直接保存 const currentBatchProgress = batchTestProgress @@ -314,7 +325,7 @@ export function FpsTest() { if (testNote) { batchNote = batchNote ? `${testNote} ${batchNote}` : testNote } - + // 如果启用了分辨率组,使用新的备注格式:[分辨率] [批量当前测试/该分辨率批量总数] if (resolutionGroupInfo && isResolutionGroupEnabled) { const { currentBatchIndex, batchCount } = resolutionGroupInfo @@ -504,18 +515,18 @@ export function FpsTest() { ): Promise => { // 验证路径是否存在且有效 if (!steam.state.steamDir || !steam.state.cs2Dir) { - addToast({ - title: "Steam 或 CS2 路径未设置,请先配置路径", - color: "warning" + addToast({ + title: "Steam 或 CS2 路径未设置,请先配置路径", + color: "warning", }) return false } - + // 验证 Steam 路径是否有效 if (!steam.state.steamDirValid) { - addToast({ - title: "Steam 路径无效,请检查路径设置", - color: "warning" + addToast({ + title: "Steam 路径无效,请检查路径设置", + color: "warning", }) return false } @@ -553,6 +564,12 @@ export function FpsTest() { testStartTimestampRef.current = `${month}/${day} ${hour}:${minute}:${second}` testStartTimeRef.current = now.getTime() + // 在测试开始时读取并记录画面设置 + if (steam.state.steamDirValid && steam.currentUser()) { + await tool.getVideoConfig(steam.state.steamDir, steam.currentUser()?.steam_id32 || 0) + testStartVideoSettingRef.current = tool.store.state.videoSetting + } + try { // 构建启动参数:基础参数 + 分辨率和全屏设置 + 自定义启动项(如果有) let baseLaunchOption = `-allow_third_party_software -condebug -conclearlog +map_workshop ${mapConfig.workshopId} ${mapConfig.map}` @@ -593,9 +610,9 @@ export function FpsTest() { }) } catch (error) { console.error("启动游戏失败:", error) - addToast({ - title: `启动游戏失败: ${error instanceof Error ? error.message : String(error)}`, - color: "danger" + addToast({ + title: `启动游戏失败: ${error instanceof Error ? error.message : String(error)}`, + color: "danger", }) return false } @@ -758,7 +775,14 @@ export function FpsTest() { if (validResults.length > 0) { const avgAvg = validResults.reduce((sum, r) => sum + (r.avg || 0), 0) / validResults.length - const avgP1 = validResults.reduce((sum, r) => sum + (r.p1 || 0), 0) / validResults.length + const avgP1 = + validResults.reduce((sum, r) => sum + (r.p1 || 0), 0) / validResults.length + + // 测试结束后读取视频设置(检测分辨率) + if (steam.state.steamDirValid && steam.currentUser()) { + await tool.getVideoConfig(steam.state.steamDir, steam.currentUser()?.steam_id32 || 0) + } + const currentVideoSetting = tool.store.state.videoSetting // 使用最后一次测试的时间戳作为平均值记录的时间戳(更准确) // 如果最后一次测试时间戳存在,使用它;否则使用当前时间 @@ -799,9 +823,7 @@ export function FpsTest() { averageNote = `${averageNote} [批量${totalTests}次平均]` // 生成唯一ID - const idTime = lastTestTimestampRef.current - ? new Date(testDate).getTime() - : Date.now() + const idTime = lastTestTimestampRef.current ? new Date(testDate).getTime() : Date.now() fpsTest.addResult({ id: `${idTime}-${Math.random().toString(36).slice(2, 11)}`, testTime, @@ -810,10 +832,12 @@ export function FpsTest() { mapLabel: mapConfig?.label || "未知地图", avg: avgAvg, p1: avgP1, - rawResult: `分辨率${currentResolution.label}批量测试${totalTests}次平均值\n平均帧: ${avgAvg.toFixed( + rawResult: `分辨率${ + currentResolution.label + }批量测试${totalTests}次平均值\n平均帧: ${avgAvg.toFixed(1)}\nP1低帧: ${avgP1.toFixed( 1 - )}\nP1低帧: ${avgP1.toFixed(1)}`, - videoSetting: tool.store.state.videoSetting, + )}`, + videoSetting: currentVideoSetting, hardwareInfo: getHardwareInfoObject(), note: averageNote, }) @@ -874,6 +898,12 @@ export function FpsTest() { validResults.reduce((sum, r) => sum + (r.avg || 0), 0) / validResults.length const avgP1 = validResults.reduce((sum, r) => sum + (r.p1 || 0), 0) / validResults.length + // 测试结束后读取视频设置(检测分辨率) + if (steam.state.steamDirValid && steam.currentUser()) { + await tool.getVideoConfig(steam.state.steamDir, steam.currentUser()?.steam_id32 || 0) + } + const currentVideoSetting = tool.store.state.videoSetting + // 使用最后一次测试的时间戳作为平均值记录的时间戳(更准确) let testTime: string let testDate: string @@ -909,9 +939,7 @@ export function FpsTest() { : `[批量${totalTests}次平均]` // 生成唯一ID - const idTime = lastTestTimestampRef.current - ? new Date(testDate).getTime() - : Date.now() + const idTime = lastTestTimestampRef.current ? new Date(testDate).getTime() : Date.now() fpsTest.addResult({ id: `${idTime}-${Math.random().toString(36).slice(2, 11)}`, testTime, @@ -923,7 +951,7 @@ export function FpsTest() { rawResult: `批量测试${totalTests}次平均值\n平均帧: ${avgAvg.toFixed( 1 )}\nP1低帧: ${avgP1.toFixed(1)}`, - videoSetting: tool.store.state.videoSetting, + videoSetting: currentVideoSetting, hardwareInfo: getHardwareInfoObject(), note: averageNote, }) @@ -999,11 +1027,21 @@ export function FpsTest() { {showResultsTable && ( <> - - @@ -1221,4 +1259,3 @@ export function FpsTest() { ) } - diff --git a/src/components/cstb/FpsTest/components/ResolutionConfig.tsx b/src/components/cstb/FpsTest/components/ResolutionConfig.tsx index 41534be..694a38c 100644 --- a/src/components/cstb/FpsTest/components/ResolutionConfig.tsx +++ b/src/components/cstb/FpsTest/components/ResolutionConfig.tsx @@ -145,10 +145,10 @@ export function ResolutionConfig({
{/* 主体:宽高输入框 + 全屏按钮(始终显示) */} -
+
x
- - {/* 启动项占满一行,右侧放置分辨率和全屏切换 */} -
- {/* 自定义启动项 */} -
- -
- -
-
-
) } diff --git a/src/components/cstb/FpsTest/components/TestResultsTable.tsx b/src/components/cstb/FpsTest/components/TestResultsTable.tsx index d568594..6c39322 100644 --- a/src/components/cstb/FpsTest/components/TestResultsTable.tsx +++ b/src/components/cstb/FpsTest/components/TestResultsTable.tsx @@ -11,7 +11,6 @@ import { import { Delete } from "@icon-park/react" import { addToast } from "@heroui/react" import { NoteCell } from "./NoteCell" -import { formatVideoSettingSummary } from "../utils/csv-export" import type { FpsTestResult } from "@/store/fps_test" import type { useFpsTestStore } from "@/store/fps_test" @@ -42,17 +41,17 @@ export function TestResultsTable({ 测试时间 地图 + 分辨率 平均帧 P1低帧 CPU - 系统版本 GPU 内存 内存频率 + 系统版本 主板型号 主板版本 BIOS版本 - 视频设置 备注 操作 @@ -67,6 +66,11 @@ export function TestResultsTable({
{result.mapLabel}
+ + {result.videoSetting + ? `${result.videoSetting.defaultres}x${result.videoSetting.defaultresheight}` + : "N/A"} + {result.avg !== null ? `${result.avg.toFixed(1)}` : "N/A"} @@ -84,9 +88,6 @@ export function TestResultsTable({ - -
{result.hardwareInfo?.os || "N/A"}
-
{result.hardwareInfo?.gpu || "N/A"}
@@ -100,6 +101,9 @@ export function TestResultsTable({ ? `${result.hardwareInfo.memorySpeed}MHz` : "N/A"} + +
{result.hardwareInfo?.os || "N/A"}
+
{result.hardwareInfo?.biosVersion || "N/A"}
- - - - {result.videoSetting - ? `${result.videoSetting.defaultres}x${result.videoSetting.defaultresheight}` - : "N/A"} - - - (null) const [downloadProgress, setDownloadProgress] = useState(0) const [installerPath, setInstallerPath] = useState(null) - const { isOpen, onOpen, onOpenChange } = useDisclosure() + const [downloadCompleted, setDownloadCompleted] = useState(false) + const { isOpen: isChangelogOpen, onOpen: onChangelogOpen, onOpenChange: onChangelogOpenChange } = useDisclosure() + + // 监听下载进度事件 + useEffect(() => { + const unlisten = listen("update-download-progress", (event) => { + const progress = event.payload + setDownloadProgress(progress) + + // 如果进度达到 100%,标记下载完成 + if (progress === 100) { + setDownloading(false) + setDownloadCompleted(true) + } + }) + + return () => { + unlisten.then(fn => fn()) + } + }, []) // 检查更新 const handleCheckUpdate = async () => { @@ -36,24 +56,32 @@ export function UpdateChecker({ customEndpoint, githubRepo }: UpdateCheckerProps setUpdateInfo(null) setDownloadProgress(0) setInstallerPath(null) + setDownloading(false) + setDownloadCompleted(false) try { + // 如果有自定义端点,使用自定义端点;否则使用默认端点(GitHub Release 或镜像源) + const endpoint = customEndpoint || null const result = await invoke("check_app_update", { - customEndpoint: customEndpoint || null, - githubRepo: githubRepo || null, + endpoint: endpoint, + useMirror: useMirror, + includePrerelease: includePrerelease, }) if (result) { setUpdateInfo(result) + // 更新 store 中的更新状态和最新版本号 app.setHasUpdate(true) - onOpen() + app.setLatestVersion(result.version) addToast({ title: "发现新版本", description: `版本 ${result.version} 可用`, color: "success", }) } else { + // 没有更新,更新 store 状态 app.setHasUpdate(false) + app.setLatestVersion("") addToast({ title: "已是最新版本", color: "default", @@ -77,30 +105,66 @@ export function UpdateChecker({ customEndpoint, githubRepo }: UpdateCheckerProps setDownloading(true) setDownloadProgress(0) + setDownloadCompleted(false) try { - // 注意:这里没有实现进度回调,实际项目中可以使用事件监听 + // 如果使用镜像源,给下载链接前面套一个 CDN 加速 + let downloadUrl = updateInfo.download_url + if (useMirror) { + downloadUrl = `https://cdn.upup.cool/${downloadUrl}` + } + + // 打印最终下载链接 + console.log("[下载更新] 最终下载链接:", downloadUrl) + const path = await invoke("download_app_update", { - downloadUrl: updateInfo.download_url, + downloadUrl: downloadUrl, }) setInstallerPath(path) setDownloadProgress(100) + setDownloading(false) + setDownloadCompleted(true) + addToast({ title: "下载完成", - description: "准备安装更新", + description: "可以点击安装按钮进行安装", color: "success", }) } catch (error) { console.error("下载更新失败:", error) - addToast({ - title: "下载失败", - description: String(error), - color: "danger", - }) + const errorMsg = String(error) + if (errorMsg.includes("取消")) { + addToast({ + title: "下载已取消", + color: "default", + }) + } else { + addToast({ + title: "下载失败", + description: errorMsg, + color: "danger", + }) + } setDownloadProgress(0) - } finally { setDownloading(false) + setDownloadCompleted(false) + } + } + + // 取消下载 + const handleCancelDownload = async () => { + try { + await invoke("cancel_download_update") + setDownloading(false) + setDownloadProgress(0) + setDownloadCompleted(false) + addToast({ + title: "已取消下载", + color: "default", + }) + } catch (error) { + console.error("取消下载失败:", error) } } @@ -119,7 +183,6 @@ export function UpdateChecker({ customEndpoint, githubRepo }: UpdateCheckerProps color: "success", }) - // 等待一小段时间后重启 setTimeout(async () => { await relaunch() }, 1000) @@ -133,18 +196,6 @@ export function UpdateChecker({ customEndpoint, githubRepo }: UpdateCheckerProps } } - // 格式化更新说明(Markdown 转 HTML) - const formatNotes = (notes?: string) => { - if (!notes) return "无更新说明" - // 简单的 Markdown 处理:换行 - return notes.split("\n").map((line, i) => ( - - {line} -
-
- )) - } - return (
@@ -155,79 +206,112 @@ export function UpdateChecker({ customEndpoint, githubRepo }: UpdateCheckerProps startContent={checking ? undefined : } isLoading={checking} onPress={handleCheckUpdate} + className="w-fit" > {checking ? "检查中..." : "检查更新"} - {app.state.hasUpdate && ( - - - 有新版本可用 - + + {updateInfo && ( + <> + {!downloading && !installerPath && ( + + )} + + {downloading && ( + + )} + + {installerPath && ( + + )} + + + + {(downloading || downloadProgress > 0 || downloadCompleted) && ( +
+ {downloadCompleted ? ( + <> + + + 下载完成 + + + ) : ( + <> + + + {downloadProgress}% + + + )} +
+ )} + )}
- {downloading && ( -
- -

正在下载更新...

-
- )} - - + {/* 更新日志对话框 */} + {(onClose) => ( <> -
- 发现新版本 - v{updateInfo?.version} -
+ 更新日志 v{updateInfo?.version}
-
-
-

更新说明:

-
- {formatNotes(updateInfo?.notes)} -
+ {updateInfo?.notes ? ( +
+ {updateInfo.notes}
- {updateInfo?.pub_date && ( -

发布时间:{new Date(updateInfo.pub_date).toLocaleString("zh-CN")}

- )} -
+ ) : ( +

暂无更新日志

+ )} - - {!downloading && !installerPath && ( - - )} - {installerPath && ( - - )} )} diff --git a/src/store/app.ts b/src/store/app.ts index 6e3d37b..8c56be1 100644 --- a/src/store/app.ts +++ b/src/store/app.ts @@ -7,9 +7,11 @@ import { LazyStore } from '@tauri-apps/plugin-store'; const defaultValue = { version: "0.0.1", hasUpdate: false, + latestVersion: "", // 最新版本号 inited: false, notice: "", - useMirror: true, + useMirror: true, // 默认使用镜像源(CDN 加速) + includePrerelease: false, // 默认不包含预发布版本 autoStart: false, startHidden: false, hiddenOnClose: false, @@ -27,9 +29,11 @@ export const useAppStore = () => { store: appStore, setVersion, setHasUpdate, + setLatestVersion, setInited, setNotice, setUseMirror, + setIncludePrerelease, setAutoStart, setStartHidden, setHiddenOnClose, @@ -47,6 +51,9 @@ const setVersion = (version: string) => { const setHasUpdate = (hasUpdate: boolean) => { appStore.state.hasUpdate = hasUpdate } +const setLatestVersion = (latestVersion: string) => { + appStore.state.latestVersion = latestVersion +} const setInited = (inited: boolean) => { appStore.state.inited = inited } @@ -56,6 +63,9 @@ const setNotice = (notice: string) => { const setUseMirror = (useMirror: boolean) => { appStore.state.useMirror = useMirror } +const setIncludePrerelease = (includePrerelease: boolean) => { + appStore.state.includePrerelease = includePrerelease +} const setAutoStart = (autoStart: boolean) => { if (autoStart) { @@ -84,9 +94,11 @@ const setSteamUsersViewMode = (viewMode: "card" | "list" | "list-large") => { const resetAppStore = () => { setVersion(defaultValue.version) setHasUpdate(defaultValue.hasUpdate) + setLatestVersion(defaultValue.latestVersion) setInited(defaultValue.inited) setNotice(defaultValue.notice) setUseMirror(defaultValue.useMirror) + setIncludePrerelease(defaultValue.includePrerelease) setAutoStart(defaultValue.autoStart) void setStartHidden(defaultValue.startHidden) setHiddenOnClose(defaultValue.hiddenOnClose)