[feat] more hw info and update feature
This commit is contained in:
@@ -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<T, String = ()> = Result<T, String>;
|
||||
|
||||
// 全局下载取消标志
|
||||
static DOWNLOAD_CANCELLED: OnceLock<Arc<AtomicBool>> = OnceLock::new();
|
||||
|
||||
fn get_download_cancelled() -> Arc<AtomicBool> {
|
||||
DOWNLOAD_CANCELLED.get_or_init(|| Arc::new(AtomicBool::new(false))).clone()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn greet(name: &str) -> Result<String, String> {
|
||||
Ok(format!("Hello, {}! You've been greeted from Rust!", name))
|
||||
@@ -301,20 +310,46 @@ pub fn read_vprof_report(console_log_path: &str) -> Result<String, String> {
|
||||
|
||||
// 更新相关命令
|
||||
|
||||
/// 检查更新
|
||||
/// 检查更新(支持 GitHub Release 和自定义端点)
|
||||
#[tauri::command]
|
||||
pub async fn check_app_update(
|
||||
app: tauri::AppHandle,
|
||||
custom_endpoint: Option<String>,
|
||||
github_repo: Option<String>,
|
||||
endpoint: Option<String>,
|
||||
use_mirror: Option<bool>,
|
||||
include_prerelease: Option<bool>,
|
||||
) -> Result<Option<UpdateInfo>, 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<String, String> {
|
||||
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<Vec<MonitorInfo>, 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<Vec<MonitorInfo>, String> {
|
||||
.await;
|
||||
|
||||
// 获取刷新率信息
|
||||
let refresh_output = Command::new("powershell")
|
||||
let _refresh_output = Command::new("powershell")
|
||||
.args(&[
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<String>,
|
||||
pub pub_date: Option<String>,
|
||||
pub download_url: String,
|
||||
pub signature: Option<String>,
|
||||
}
|
||||
|
||||
/// 自定义更新服务器响应格式
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CustomUpdateResponse {
|
||||
/// gh-info API 响应结构
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct GhInfoApiResponse {
|
||||
repo: String,
|
||||
latest_version: String,
|
||||
changelog: Option<String>,
|
||||
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<String>,
|
||||
#[serde(rename = "pub_date")]
|
||||
pub_date: Option<String>,
|
||||
download_url: String,
|
||||
signature: Option<String>,
|
||||
platforms: Option<PlatformDownloads>,
|
||||
platforms: Option<std::collections::HashMap<String, PlatformInfo>>,
|
||||
}
|
||||
|
||||
/// 平台特定的下载链接
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct PlatformDownloads {
|
||||
#[serde(rename = "windows-x86_64")]
|
||||
windows_x86_64: Option<PlatformInfo>,
|
||||
#[serde(rename = "darwin-x86_64")]
|
||||
darwin_x86_64: Option<PlatformInfo>,
|
||||
#[serde(rename = "darwin-aarch64")]
|
||||
darwin_aarch64: Option<PlatformInfo>,
|
||||
#[serde(rename = "linux-x86_64")]
|
||||
linux_x86_64: Option<PlatformInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
/// 平台特定信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct PlatformInfo {
|
||||
url: String,
|
||||
signature: Option<String>,
|
||||
}
|
||||
|
||||
/// GitHub Release API 响应
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GitHubRelease {
|
||||
tag_name: String,
|
||||
name: Option<String>,
|
||||
body: Option<String>,
|
||||
published_at: Option<String>,
|
||||
assets: Vec<GitHubAsset>,
|
||||
}
|
||||
|
||||
#[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<Option<UpdateInfo>> {
|
||||
// 首先尝试自定义服务器
|
||||
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<Option<UpdateInfo>> {
|
||||
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::<CustomUpdateApiResponse>(&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<Option<UpdateInfo>> {
|
||||
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<String> {
|
||||
// 尝试解析为字符串数组格式: ["URL1", "URL2", ...]
|
||||
if let Ok(urls) = serde_json::from_value::<Vec<String>>(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::<Vec<Vec<String>>>(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<String> {
|
||||
#[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<u32>, Option<String>) {
|
||||
// 去掉 '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<u32> = 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::<u32>().ok()).unwrap_or(0);
|
||||
let v2_part = v2_parts.get(i).and_then(|s| s.parse::<u32>().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<String>, current: &Option<String>) -> 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<u32> {
|
||||
// 尝试从最后一部分提取数字
|
||||
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<Box<dyn Fn(u64, u64) + Send + Sync>>,
|
||||
cancelled: Arc<AtomicBool>,
|
||||
) -> Result<PathBuf> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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 (
|
||||
<section className="flex flex-col gap-4 overflow-hidden">
|
||||
<div className="flex flex-col items-start gap-4 pt-2 pb-1">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm">版本号:{app.state.version}</p>
|
||||
<p className="text-sm">是否有更新:{app.state.hasUpdate ? "有" : "无"}</p>
|
||||
<p className="text-sm">是否使用镜像源:{app.state.useMirror ? "是" : "否"}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm">版本号:{app.state.version}</p>
|
||||
{app.state.hasUpdate && app.state.latestVersion && (
|
||||
<Chip size="sm" color="success" variant="flat">
|
||||
{app.state.latestVersion}
|
||||
</Chip>
|
||||
)}
|
||||
</div>
|
||||
{/* <p className="text-sm">是否有更新:{app.state.hasUpdate ? "有" : "无"}</p> */}
|
||||
{/* <p className="text-sm">是否使用镜像源:{app.state.useMirror ? "是" : "否"}</p> */}
|
||||
</div>
|
||||
|
||||
{/* <div className="w-full pt-4 border-t border-zinc-200 dark:border-zinc-800">
|
||||
<div className="w-full pt-4 border-t border-zinc-200 dark:border-zinc-800">
|
||||
<h3 className="mb-3 text-sm font-semibold">更新检查</h3>
|
||||
<UpdateChecker customEndpoint={customEndpoint} githubRepo={githubRepo} />
|
||||
</div> */}
|
||||
<div className="mb-3 space-y-3">
|
||||
{/* <Switch
|
||||
isSelected={app.state.useMirror}
|
||||
size="sm"
|
||||
onChange={(e) => app.setUseMirror(e.target.checked)}
|
||||
>
|
||||
使用镜像源
|
||||
</Switch> */}
|
||||
{/* <p className="text-xs text-zinc-500">
|
||||
{app.state.useMirror
|
||||
? "使用自建更新服务检查更新"
|
||||
: "使用 GitHub Release 检查更新"}
|
||||
</p> */}
|
||||
<Switch
|
||||
isSelected={app.state.includePrerelease}
|
||||
size="sm"
|
||||
onChange={(e) => app.setIncludePrerelease(e.target.checked)}
|
||||
>
|
||||
包含测试版
|
||||
</Switch>
|
||||
<p className="text-xs text-zinc-500">
|
||||
{app.state.includePrerelease
|
||||
? "检查更新时会包含预发布版本(beta、alpha等)"
|
||||
: "仅检查正式版本"}
|
||||
</p>
|
||||
</div>
|
||||
<UpdateChecker
|
||||
useMirror={app.state.useMirror}
|
||||
customEndpoint={customEndpoint || undefined}
|
||||
includePrerelease={app.state.includePrerelease}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col w-full pt-4 space-y-3 border-t border-zinc-200 dark:border-zinc-800">
|
||||
<h3 className="mb-3 text-sm font-semibold">启动设置</h3>
|
||||
|
||||
@@ -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<typeof tool.state.videoSetting | null>(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<string | null>(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<boolean> => {
|
||||
// 验证路径是否存在且有效
|
||||
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() {
|
||||
<BatchTestProgress progress={batchTestProgress} />
|
||||
{showResultsTable && (
|
||||
<>
|
||||
<Button size="sm" variant="flat" onPress={handleExportAverageCSVWrapper} className="font-medium">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={handleExportAverageCSVWrapper}
|
||||
className="font-medium"
|
||||
>
|
||||
<DownloadOne size={14} />
|
||||
仅导出平均结果
|
||||
</Button>
|
||||
<Button size="sm" variant="flat" onPress={handleExportCSVWrapper} className="font-medium">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={handleExportCSVWrapper}
|
||||
className="font-medium"
|
||||
>
|
||||
<DownloadOne size={14} />
|
||||
导出CSV
|
||||
</Button>
|
||||
@@ -1221,4 +1259,3 @@ export function FpsTest() {
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -145,10 +145,10 @@ export function ResolutionConfig({
|
||||
</div>
|
||||
</div>
|
||||
{/* 主体:宽高输入框 + 全屏按钮(始终显示) */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 ">
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
size="sm"
|
||||
size="md"
|
||||
type="number"
|
||||
placeholder="宽"
|
||||
value={resolutionWidth}
|
||||
@@ -162,7 +162,7 @@ export function ResolutionConfig({
|
||||
/>
|
||||
<span className="text-xs text-default-400">x</span>
|
||||
<Input
|
||||
size="sm"
|
||||
size="md"
|
||||
type="number"
|
||||
placeholder="高"
|
||||
value={resolutionHeight}
|
||||
@@ -176,7 +176,7 @@ export function ResolutionConfig({
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
size="md"
|
||||
variant={isFullscreen ? "solid" : "flat"}
|
||||
color={isFullscreen ? "primary" : "default"}
|
||||
onPress={() => fpsTest.setIsFullscreen(!isFullscreen)}
|
||||
|
||||
@@ -88,24 +88,6 @@ export function TestConfigPanel({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 启动项占满一行,右侧放置分辨率和全屏切换 */}
|
||||
<div className="flex items-start gap-4">
|
||||
{/* 自定义启动项 */}
|
||||
<div className="flex flex-col flex-1 gap-1.5">
|
||||
<label className="h-5 text-xs text-default-500">自定义启动项</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
size="md"
|
||||
placeholder="输入自定义启动参数(可选)"
|
||||
value={customLaunchOption}
|
||||
onValueChange={onCustomLaunchOptionChange}
|
||||
isDisabled={isMonitoring}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
<TableHeader>
|
||||
<TableColumn minWidth={140}>测试时间</TableColumn>
|
||||
<TableColumn width={40}>地图</TableColumn>
|
||||
<TableColumn width={100}>分辨率</TableColumn>
|
||||
<TableColumn width={60}>平均帧</TableColumn>
|
||||
<TableColumn width={60}>P1低帧</TableColumn>
|
||||
<TableColumn width={100}>CPU</TableColumn>
|
||||
<TableColumn minWidth={80}>系统版本</TableColumn>
|
||||
<TableColumn minWidth={100}>GPU</TableColumn>
|
||||
<TableColumn width={80}>内存</TableColumn>
|
||||
<TableColumn width={80}>内存频率</TableColumn>
|
||||
<TableColumn minWidth={80}>系统版本</TableColumn>
|
||||
<TableColumn width={100}>主板型号</TableColumn>
|
||||
<TableColumn minWidth={80}>主板版本</TableColumn>
|
||||
<TableColumn minWidth={80}>BIOS版本</TableColumn>
|
||||
<TableColumn width={120}>视频设置</TableColumn>
|
||||
<TableColumn minWidth={40}>备注</TableColumn>
|
||||
<TableColumn width={60} align="center">
|
||||
操作
|
||||
@@ -67,6 +66,11 @@ export function TestResultsTable({
|
||||
<TableCell className="text-xs">
|
||||
<div className="truncate">{result.mapLabel}</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs whitespace-nowrap">
|
||||
{result.videoSetting
|
||||
? `${result.videoSetting.defaultres}x${result.videoSetting.defaultresheight}`
|
||||
: "N/A"}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{result.avg !== null ? `${result.avg.toFixed(1)}` : "N/A"}
|
||||
</TableCell>
|
||||
@@ -84,9 +88,6 @@ export function TestResultsTable({
|
||||
</div>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
<div className="truncate">{result.hardwareInfo?.os || "N/A"}</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
<div className="truncate">{result.hardwareInfo?.gpu || "N/A"}</div>
|
||||
</TableCell>
|
||||
@@ -100,6 +101,9 @@ export function TestResultsTable({
|
||||
? `${result.hardwareInfo.memorySpeed}MHz`
|
||||
: "N/A"}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
<div className="truncate">{result.hardwareInfo?.os || "N/A"}</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
<Tooltip
|
||||
content={result.hardwareInfo?.motherboardModel || "N/A"}
|
||||
@@ -117,15 +121,6 @@ export function TestResultsTable({
|
||||
<TableCell className="text-xs">
|
||||
<div className="truncate">{result.hardwareInfo?.biosVersion || "N/A"}</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
<Tooltip content={formatVideoSettingSummary(result.videoSetting)}>
|
||||
<span className="truncate cursor-help">
|
||||
{result.videoSetting
|
||||
? `${result.videoSetting.defaultres}x${result.videoSetting.defaultresheight}`
|
||||
: "N/A"}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs min-w-fit">
|
||||
<NoteCell
|
||||
note={result.note || ""}
|
||||
|
||||
@@ -173,8 +173,16 @@ export async function readResult(
|
||||
memory: hardwareInfo.total_memory
|
||||
? Math.round(hardwareInfo.total_memory / 1024 / 1024 / 1024)
|
||||
: null,
|
||||
memoryManufacturer: null,
|
||||
memorySpeed: null,
|
||||
memoryDefaultSpeed: null,
|
||||
gpu: null,
|
||||
monitor: null,
|
||||
monitorManufacturer: null,
|
||||
monitorModel: null,
|
||||
motherboardModel: null,
|
||||
motherboardVersion: null,
|
||||
biosVersion: null,
|
||||
}
|
||||
: null,
|
||||
note: batchNote,
|
||||
@@ -208,8 +216,16 @@ export async function readResult(
|
||||
memory: hardwareInfo.total_memory
|
||||
? Math.round(hardwareInfo.total_memory / 1024 / 1024 / 1024)
|
||||
: null,
|
||||
memoryManufacturer: null,
|
||||
memorySpeed: null,
|
||||
memoryDefaultSpeed: null,
|
||||
gpu: null,
|
||||
monitor: null,
|
||||
monitorManufacturer: null,
|
||||
monitorModel: null,
|
||||
motherboardModel: null,
|
||||
motherboardVersion: null,
|
||||
biosVersion: null,
|
||||
}
|
||||
: null,
|
||||
note: singleNote, // 保存备注(包含分辨率信息)
|
||||
|
||||
@@ -45,6 +45,17 @@ export async function handleExportCSV(
|
||||
"分辨率",
|
||||
"视频设置",
|
||||
"备注",
|
||||
"光影质量",
|
||||
"纹理过滤质量",
|
||||
"多重采样抗锯齿",
|
||||
"CMAA抗锯齿",
|
||||
"阴影质量",
|
||||
"动态阴影",
|
||||
"纹理细节",
|
||||
"粒子细节",
|
||||
"环境光遮蔽",
|
||||
"HDR细节",
|
||||
"FSR细节",
|
||||
]
|
||||
|
||||
const csvRows = [headers.join(",")]
|
||||
@@ -68,6 +79,17 @@ export async function handleExportCSV(
|
||||
: "N/A",
|
||||
`"${formatVideoSettingSummary(result.videoSetting)}"`,
|
||||
`"${result.note || ""}"`,
|
||||
result.videoSetting?.shaderquality || "N/A",
|
||||
result.videoSetting?.r_texturefilteringquality || "N/A",
|
||||
result.videoSetting?.msaa_samples || "N/A",
|
||||
result.videoSetting?.r_csgo_cmaa_enable || "N/A",
|
||||
result.videoSetting?.videocfg_shadow_quality || "N/A",
|
||||
result.videoSetting?.videocfg_dynamic_shadows || "N/A",
|
||||
result.videoSetting?.videocfg_texture_detail || "N/A",
|
||||
result.videoSetting?.videocfg_particle_detail || "N/A",
|
||||
result.videoSetting?.videocfg_ao_detail || "N/A",
|
||||
result.videoSetting?.videocfg_hdr_detail || "N/A",
|
||||
result.videoSetting?.videocfg_fsr_detail || "N/A",
|
||||
]
|
||||
csvRows.push(row.join(","))
|
||||
}
|
||||
@@ -133,6 +155,17 @@ export async function handleExportAverageCSV(
|
||||
"分辨率",
|
||||
"视频设置",
|
||||
"备注",
|
||||
"光影质量",
|
||||
"纹理过滤质量",
|
||||
"多重采样抗锯齿",
|
||||
"CMAA抗锯齿",
|
||||
"阴影质量",
|
||||
"动态阴影",
|
||||
"纹理细节",
|
||||
"粒子细节",
|
||||
"环境光遮蔽",
|
||||
"HDR细节",
|
||||
"FSR细节",
|
||||
]
|
||||
|
||||
const csvRows = [headers.join(",")]
|
||||
@@ -156,6 +189,17 @@ export async function handleExportAverageCSV(
|
||||
: "N/A",
|
||||
`"${formatVideoSettingSummary(result.videoSetting)}"`,
|
||||
`"${result.note || ""}"`,
|
||||
result.videoSetting?.shaderquality || "N/A",
|
||||
result.videoSetting?.r_texturefilteringquality || "N/A",
|
||||
result.videoSetting?.msaa_samples || "N/A",
|
||||
result.videoSetting?.r_csgo_cmaa_enable || "N/A",
|
||||
result.videoSetting?.videocfg_shadow_quality || "N/A",
|
||||
result.videoSetting?.videocfg_dynamic_shadows || "N/A",
|
||||
result.videoSetting?.videocfg_texture_detail || "N/A",
|
||||
result.videoSetting?.videocfg_particle_detail || "N/A",
|
||||
result.videoSetting?.videocfg_ao_detail || "N/A",
|
||||
result.videoSetting?.videocfg_hdr_detail || "N/A",
|
||||
result.videoSetting?.videocfg_fsr_detail || "N/A",
|
||||
]
|
||||
csvRows.push(row.join(","))
|
||||
}
|
||||
|
||||
@@ -1,34 +1,54 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button, Progress, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/react"
|
||||
import { Download, Refresh, CheckCorrect } from "@icon-park/react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { Button, CircularProgress, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/react"
|
||||
import { Download, Refresh, FileText, Close, Check } from "@icon-park/react"
|
||||
import { invoke } from "@tauri-apps/api/core"
|
||||
import { listen } from "@tauri-apps/api/event"
|
||||
import { relaunch } from "@tauri-apps/plugin-process"
|
||||
import { addToast } from "@heroui/react"
|
||||
import { useAppStore } from "@/store/app"
|
||||
import { MarkdownRender } from "@/components/markdown"
|
||||
|
||||
interface UpdateInfo {
|
||||
version: string
|
||||
notes?: string
|
||||
pub_date?: string
|
||||
download_url: string
|
||||
signature?: string
|
||||
}
|
||||
|
||||
interface UpdateCheckerProps {
|
||||
useMirror?: boolean
|
||||
customEndpoint?: string
|
||||
githubRepo?: string
|
||||
includePrerelease?: boolean
|
||||
}
|
||||
|
||||
export function UpdateChecker({ customEndpoint, githubRepo }: UpdateCheckerProps) {
|
||||
export function UpdateChecker({ useMirror = true, customEndpoint, includePrerelease = false }: UpdateCheckerProps) {
|
||||
const app = useAppStore()
|
||||
const [checking, setChecking] = useState(false)
|
||||
const [downloading, setDownloading] = useState(false)
|
||||
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null)
|
||||
const [downloadProgress, setDownloadProgress] = useState(0)
|
||||
const [installerPath, setInstallerPath] = useState<string | null>(null)
|
||||
const { isOpen, onOpen, onOpenChange } = useDisclosure()
|
||||
const [downloadCompleted, setDownloadCompleted] = useState(false)
|
||||
const { isOpen: isChangelogOpen, onOpen: onChangelogOpen, onOpenChange: onChangelogOpenChange } = useDisclosure()
|
||||
|
||||
// 监听下载进度事件
|
||||
useEffect(() => {
|
||||
const unlisten = listen<number>("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<UpdateInfo | null>("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<string>("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) => (
|
||||
<span key={i}>
|
||||
{line}
|
||||
<br />
|
||||
</span>
|
||||
))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -155,79 +206,112 @@ export function UpdateChecker({ customEndpoint, githubRepo }: UpdateCheckerProps
|
||||
startContent={checking ? undefined : <Refresh />}
|
||||
isLoading={checking}
|
||||
onPress={handleCheckUpdate}
|
||||
className="w-fit"
|
||||
>
|
||||
{checking ? "检查中..." : "检查更新"}
|
||||
</Button>
|
||||
{app.state.hasUpdate && (
|
||||
<span className="flex items-center gap-1 text-sm text-green-600 dark:text-green-400">
|
||||
<CheckCorrect size={16} />
|
||||
有新版本可用
|
||||
</span>
|
||||
|
||||
{updateInfo && (
|
||||
<>
|
||||
{!downloading && !installerPath && (
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
startContent={<Download />}
|
||||
onPress={handleDownloadUpdate}
|
||||
className="w-fit"
|
||||
>
|
||||
下载更新
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{downloading && (
|
||||
<Button
|
||||
size="sm"
|
||||
color="danger"
|
||||
variant="flat"
|
||||
startContent={<Close />}
|
||||
onPress={handleCancelDownload}
|
||||
className="w-fit"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{installerPath && (
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
onPress={handleInstallUpdate}
|
||||
className="w-fit"
|
||||
>
|
||||
安装更新
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
color="default"
|
||||
variant="flat"
|
||||
startContent={<FileText />}
|
||||
onPress={onChangelogOpen}
|
||||
className="w-fit"
|
||||
>
|
||||
更新日志
|
||||
</Button>
|
||||
|
||||
{(downloading || downloadProgress > 0 || downloadCompleted) && (
|
||||
<div className="flex items-center gap-1">
|
||||
{downloadCompleted ? (
|
||||
<>
|
||||
<Check className="text-green-500 dark:text-green-400" size={14} />
|
||||
<span className="text-xs text-green-500 dark:text-green-400">
|
||||
下载完成
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CircularProgress
|
||||
aria-label="下载进度"
|
||||
value={downloadProgress}
|
||||
color="primary"
|
||||
size="sm"
|
||||
showValueLabel={false}
|
||||
/>
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{downloadProgress}%
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{downloading && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Progress
|
||||
aria-label="下载进度"
|
||||
value={downloadProgress}
|
||||
color="primary"
|
||||
showValueLabel
|
||||
className="max-w-full"
|
||||
/>
|
||||
<p className="text-xs text-zinc-500">正在下载更新...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange} size="lg">
|
||||
{/* 更新日志对话框 */}
|
||||
<Modal isOpen={isChangelogOpen} onOpenChange={onChangelogOpenChange} size="lg">
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>发现新版本</span>
|
||||
<span className="text-sm font-normal text-zinc-500">v{updateInfo?.version}</span>
|
||||
</div>
|
||||
<span>更新日志 v{updateInfo?.version}</span>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="mb-1 text-sm font-semibold">更新说明:</p>
|
||||
<div className="text-sm whitespace-pre-wrap text-zinc-600 dark:text-zinc-400">
|
||||
{formatNotes(updateInfo?.notes)}
|
||||
</div>
|
||||
{updateInfo?.notes ? (
|
||||
<div className="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<MarkdownRender>{updateInfo.notes}</MarkdownRender>
|
||||
</div>
|
||||
{updateInfo?.pub_date && (
|
||||
<p className="text-xs text-zinc-500">发布时间:{new Date(updateInfo.pub_date).toLocaleString("zh-CN")}</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-zinc-500">暂无更新日志</p>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="light" onPress={onClose}>
|
||||
取消
|
||||
<Button color="primary" variant="flat" onPress={onClose}>
|
||||
关闭
|
||||
</Button>
|
||||
{!downloading && !installerPath && (
|
||||
<Button
|
||||
color="primary"
|
||||
startContent={<Download />}
|
||||
onPress={async () => {
|
||||
await handleDownloadUpdate()
|
||||
}}
|
||||
>
|
||||
下载更新
|
||||
</Button>
|
||||
)}
|
||||
{installerPath && (
|
||||
<Button
|
||||
color="primary"
|
||||
onPress={async () => {
|
||||
await handleInstallUpdate()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
立即安装
|
||||
</Button>
|
||||
)}
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user