From 41008cf13c9f2fe82ac8ce2a89d91fee08d59262 Mon Sep 17 00:00:00 2001 From: purp1e Date: Wed, 5 Nov 2025 11:19:43 +0800 Subject: [PATCH] [feat] better installer + changelog test version switch + better video config view --- UPDATE_API.md | 248 ++++++++++++ UPDATE_USAGE.md | 181 +++++++++ next-env.d.ts | 2 +- src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 3 +- src-tauri/src/cmds.rs | 45 +++ src-tauri/src/tool/common.rs | 34 ++ src-tauri/src/tool/mod.rs | 1 + src-tauri/src/tool/updater.rs | 429 +++++++++++++++++++++ src-tauri/src/vdf/preset.rs | 4 +- src/app/(main)/dynamic/page.tsx | 132 ++++--- src/app/(main)/preference/general/page.tsx | 68 ++-- src/app/providers.tsx | 3 +- src/components/cstb/FpsTest.tsx | 2 +- src/components/cstb/UpdateChecker.tsx | 238 ++++++++++++ src/components/cstb/VideoSetting.tsx | 234 ++++++----- src/components/window/Nav.tsx | 7 + src/components/window/SideBar.tsx | 48 ++- src/store/index.ts | 2 + 19 files changed, 1499 insertions(+), 183 deletions(-) create mode 100644 UPDATE_API.md create mode 100644 UPDATE_USAGE.md create mode 100644 src-tauri/src/tool/updater.rs create mode 100644 src/components/cstb/UpdateChecker.tsx diff --git a/UPDATE_API.md b/UPDATE_API.md new file mode 100644 index 0000000..3a901ff --- /dev/null +++ b/UPDATE_API.md @@ -0,0 +1,248 @@ +# 更新 API 文档 + +本文档说明 CS工具箱 的更新检查接口格式和配置方法。 + +## 功能概述 + +CS工具箱 支持两种更新检查方式: +1. **自定义更新服务器**(优先) +2. **GitHub Release**(作为备用方案) + +## 配置方法 + +### 方式一:环境变量(推荐) + +在项目根目录创建 `.env.local` 文件: + +```env +NEXT_PUBLIC_UPDATE_ENDPOINT=https://your-server.com/api/update/check +NEXT_PUBLIC_GITHUB_REPO=your-username/your-repo +``` + +### 方式二:代码中配置 + +在 `src/app/(main)/preference/general/page.tsx` 中修改: + +```typescript +const customEndpoint = "https://your-server.com/api/update/check" +const githubRepo = "your-username/your-repo" +``` + +## 自定义更新服务器接口格式 + +### 请求 + +- **方法**: GET +- **URL**: 你配置的 `customEndpoint` +- **Headers**: 无特殊要求 + +### 响应格式 + +服务器应返回 JSON 格式的更新信息: + +```json +{ + "version": "0.0.7", + "notes": "## 更新内容\n\n- 修复了已知问题\n- 添加了新功能", + "pub_date": "2025-01-15T10:00:00Z", + "download_url": "https://your-server.com/releases/v0.0.7/cstb-0.0.7-x86_64.exe", + "signature": "可选:安装包签名", + "platforms": { + "windows-x86_64": { + "url": "https://your-server.com/releases/v0.0.7/cstb-0.0.7-windows-x86_64.exe", + "signature": "可选:Windows 安装包签名" + }, + "darwin-x86_64": { + "url": "https://your-server.com/releases/v0.0.7/cstb-0.0.7-darwin-x86_64.dmg", + "signature": "可选:macOS x86_64 安装包签名" + }, + "darwin-aarch64": { + "url": "https://your-server.com/releases/v0.0.7/cstb-0.0.7-darwin-aarch64.dmg", + "signature": "可选:macOS Apple Silicon 安装包签名" + }, + "linux-x86_64": { + "url": "https://your-server.com/releases/v0.0.7/cstb-0.0.7-linux-x86_64.AppImage", + "signature": "可选:Linux 安装包签名" + } + } +} +``` + +### 字段说明 + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `version` | string | 是 | 新版本号,格式:`主版本.次版本.修订版本`(如 `0.0.7`) | +| `notes` | string | 否 | 更新说明,支持 Markdown 格式 | +| `pub_date` | string | 否 | 发布时间,ISO 8601 格式(如 `2025-01-15T10:00:00Z`) | +| `download_url` | string | 是 | 默认下载链接(如果未指定平台特定链接时使用) | +| `signature` | string | 否 | 默认安装包签名 | +| `platforms` | object | 否 | 平台特定的下载信息 | + +### 平台特定配置 + +如果提供了 `platforms` 对象,系统会优先使用当前平台的特定链接。支持的平台标识: + +- `windows-x86_64`: Windows 64位 +- `darwin-x86_64`: macOS Intel +- `darwin-aarch64`: macOS Apple Silicon +- `linux-x86_64`: Linux 64位 + +### 简化格式(仅 Windows) + +如果你只需要支持 Windows,可以简化响应: + +```json +{ + "version": "0.0.7", + "notes": "更新说明", + "download_url": "https://your-server.com/releases/v0.0.7/cstb-0.0.7.exe" +} +``` + +## GitHub Release 格式 + +### 仓库要求 + +1. 在 GitHub 上创建 Release +2. Release 标签格式:`v0.0.7` 或 `0.0.7`(系统会自动移除 `v` 前缀) +3. Release 标题和说明会作为更新说明显示 + +### 资源文件命名 + +系统会自动识别以下格式的安装包: + +**Windows:** +- `.exe` +- `.msi` +- 文件名包含 `windows` 和 `x86_64` + +**macOS:** +- `.dmg` +- 文件名包含 `darwin` 和架构标识(`x86_64` 或 `aarch64`) + +**Linux:** +- `.deb` +- `.rpm` +- `.AppImage` +- 文件名包含 `linux` 和 `x86_64` + +### 推荐命名格式 + +``` +cstb-{version}-{platform}-{arch}.{ext} +``` + +例如: +- `cstb-0.0.7-windows-x86_64.exe` +- `cstb-0.0.7-darwin-x86_64.dmg` +- `cstb-0.0.7-darwin-aarch64.dmg` +- `cstb-0.0.7-linux-x86_64.AppImage` + +## 版本比较逻辑 + +系统使用简单的版本号比较: +- 按 `.`、`-`、`_` 分割版本号 +- 逐段比较数字部分 +- 如果新版本号大于当前版本,则提示更新 + +例如: +- `0.0.7` > `0.0.6` ✓ +- `0.0.6-beta.4` 与 `0.0.6` 视为相等(简单比较) + +## 更新流程 + +1. **检查更新**:用户点击"检查更新"按钮 +2. **下载更新**:如果有新版本,用户确认后开始下载 +3. **安装更新**: + - Windows: 自动运行 NSIS 安装程序(静默模式) + - macOS: 打开 DMG 文件(需用户手动安装) + - Linux: 根据文件类型执行相应安装命令 + +## 示例服务器实现 + +### Node.js/Express 示例 + +```javascript +app.get('/api/update/check', (req, res) => { + const platform = req.headers['user-agent'] || ''; + + const updateInfo = { + version: "0.0.7", + notes: "## 更新内容\n\n- 修复了已知问题", + pub_date: new Date().toISOString(), + download_url: "https://your-server.com/releases/v0.0.7/cstb-0.0.7.exe", + platforms: { + "windows-x86_64": { + url: "https://your-server.com/releases/v0.0.7/cstb-0.0.7-windows-x86_64.exe" + } + } + }; + + res.json(updateInfo); +}); +``` + +### Python/Flask 示例 + +```python +from flask import Flask, jsonify +from datetime import datetime + +app = Flask(__name__) + +@app.route('/api/update/check') +def check_update(): + return jsonify({ + "version": "0.0.7", + "notes": "## 更新内容\n\n- 修复了已知问题", + "pub_date": datetime.now().isoformat() + "Z", + "download_url": "https://your-server.com/releases/v0.0.7/cstb-0.0.7.exe", + "platforms": { + "windows-x86_64": { + "url": "https://your-server.com/releases/v0.0.7/cstb-0.0.7-windows-x86_64.exe" + } + } + }) +``` + +## 注意事项 + +1. **HTTPS**: 建议使用 HTTPS 协议以确保安全 +2. **CORS**: 如果从浏览器访问,需要配置 CORS 头 +3. **超时**: 请求超时时间为 10 秒 +4. **下载超时**: 下载超时时间为 5 分钟 +5. **缓存**: 系统不会缓存更新信息,每次检查都会请求最新数据 + +## 故障排查 + +### 自定义服务器检查失败 + +1. 检查服务器是否可访问 +2. 检查响应格式是否正确 +3. 检查 HTTP 状态码是否为 200 +4. 查看应用日志中的错误信息 + +### GitHub Release 检查失败 + +1. 确认仓库名称格式正确(`owner/repo`) +2. 确认仓库是公开的 +3. 确认已创建 Release +4. 确认 Release 中有适合当前平台的资源文件 + +### 下载失败 + +1. 检查下载链接是否有效 +2. 检查网络连接 +3. 检查磁盘空间是否充足 +4. 检查文件权限 + +## 测试 + +可以使用以下 curl 命令测试自定义服务器: + +```bash +curl https://your-server.com/api/update/check +``` + +应该返回符合格式的 JSON 响应。 diff --git a/UPDATE_USAGE.md b/UPDATE_USAGE.md new file mode 100644 index 0000000..37456d8 --- /dev/null +++ b/UPDATE_USAGE.md @@ -0,0 +1,181 @@ +# 更新功能使用说明 + +## 功能概述 + +CS工具箱 现已支持自动更新检查、下载和安装功能。系统支持两种更新源: + +1. **自定义更新服务器**(优先使用) +2. **GitHub Release**(备用方案) + +## 快速开始 + +### 1. 配置更新源 + +在项目根目录创建 `.env.local` 文件(如果不存在): + +```env +# 自定义更新服务器 URL(可选) +NEXT_PUBLIC_UPDATE_ENDPOINT=https://your-server.com/api/update/check + +# GitHub 仓库(格式:owner/repo,可选) +NEXT_PUBLIC_GITHUB_REPO=your-username/cstb-next +``` + +### 2. 使用更新功能 + +1. 打开应用 +2. 进入 **偏好设置** → **通用设置** +3. 在"更新检查"部分点击"检查更新"按钮 +4. 如果有新版本,系统会显示更新信息 +5. 点击"下载更新"开始下载 +6. 下载完成后,点击"立即安装"进行安装 + +## 功能特性 + +- ✅ 自动检测新版本 +- ✅ 支持自定义更新服务器 +- ✅ GitHub Release 作为备用方案 +- ✅ 跨平台支持(Windows、macOS、Linux) +- ✅ 下载进度显示 +- ✅ 更新说明显示(支持 Markdown) +- ✅ 自动重启应用 + +## 自定义更新服务器 + +### 接口要求 + +你的服务器需要提供一个 GET 接口,返回 JSON 格式的更新信息。 + +详细格式请参考 [UPDATE_API.md](./UPDATE_API.md) + +### 简单示例 + +```json +{ + "version": "0.0.7", + "notes": "修复了已知问题", + "pub_date": "2025-01-15T10:00:00Z", + "download_url": "https://your-server.com/releases/v0.0.7/cstb-0.0.7.exe" +} +``` + +## GitHub Release + +### 设置步骤 + +1. 在 GitHub 上创建仓库(如果还没有) +2. 创建 Release,标签格式:`v0.0.7` 或 `0.0.7` +3. 上传安装包到 Release 资源 +4. 在 `.env.local` 中配置仓库名称: + +```env +NEXT_PUBLIC_GITHUB_REPO=your-username/cstb-next +``` + +### 文件命名建议 + +- Windows: `cstb-0.0.7-windows-x86_64.exe` +- macOS Intel: `cstb-0.0.7-darwin-x86_64.dmg` +- macOS Apple Silicon: `cstb-0.0.7-darwin-aarch64.dmg` +- Linux: `cstb-0.0.7-linux-x86_64.AppImage` + +## 更新流程 + +``` +用户点击"检查更新" + ↓ +检查自定义服务器 + ↓ (失败) +检查 GitHub Release + ↓ +比较版本号 + ↓ (有新版本) +显示更新对话框 + ↓ +用户确认下载 + ↓ +下载安装包 + ↓ +安装并重启 +``` + +## 开发说明 + +### Rust 端 + +更新相关的代码位于: +- `src-tauri/src/tool/updater.rs` - 更新逻辑实现 +- `src-tauri/src/cmds.rs` - Tauri 命令接口 +- `src-tauri/src/main.rs` - 命令注册 + +### 前端 + +更新相关的代码位于: +- `src/components/cstb/UpdateChecker.tsx` - 更新检查组件 +- `src/app/(main)/preference/general/page.tsx` - 设置页面 + +### 添加新的更新源 + +如果你想添加新的更新源(如 GitLab、自建服务器等),可以修改 `src-tauri/src/tool/updater.rs` 中的 `check_update` 函数。 + +## 故障排查 + +### 检查更新失败 + +1. **确认网络连接正常** +2. **检查配置是否正确** + - 确认 `.env.local` 文件存在 + - 确认环境变量名称正确 + - 确认 URL 格式正确 +3. **查看控制台日志** + - 打开开发者工具(F12) + - 查看 Console 标签页的错误信息 + +### 下载失败 + +1. **检查下载链接是否有效** + - 在浏览器中直接访问下载链接 + - 确认文件存在且可访问 +2. **检查磁盘空间** + - 确保有足够的磁盘空间 +3. **检查文件权限** + - 确保应用有写入权限 + +### 安装失败 + +1. **Windows** + - 确认有管理员权限 + - 检查防病毒软件是否阻止安装 +2. **macOS** + - 确认在"系统偏好设置"中允许安装 + - 可能需要手动打开 DMG 文件 +3. **Linux** + - 确认有 sudo 权限 + - 检查包管理器是否正确安装 + +## 测试 + +### 测试自定义服务器 + +1. 启动本地服务器 +2. 配置 `NEXT_PUBLIC_UPDATE_ENDPOINT=http://localhost:3000/api/update` +3. 在应用中点击"检查更新" + +### 测试 GitHub Release + +1. 创建一个测试 Release +2. 配置 `NEXT_PUBLIC_GITHUB_REPO=your-username/your-repo` +3. 确保 Release 版本号高于当前版本 +4. 在应用中点击"检查更新" + +## 注意事项 + +1. **版本号格式**:建议使用语义化版本(如 `0.0.7`) +2. **HTTPS**:生产环境建议使用 HTTPS +3. **超时设置**:检查更新超时 10 秒,下载超时 5 分钟 +4. **自动重启**:安装完成后会自动重启应用 + +## 相关文档 + +- [UPDATE_API.md](./UPDATE_API.md) - 详细的 API 接口文档 +- [Tauri 官方文档](https://tauri.app/) - Tauri 框架文档 diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c..c4b7818 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index d7e1ff9..539aa78 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -8,6 +8,7 @@ version = "0.0.6" dependencies = [ "anyhow", "base64 0.22.1", + "futures-util", "log", "regex", "reqwest", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 161bbf3..c553e1d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -31,7 +31,8 @@ walkdir = "2.5.0" serde_json = "1.0.145" regex = "1.12.2" serde = { version = "1.0.228", features = ["derive"] } -reqwest = { version = "0.12.24", features = ["blocking"] } +reqwest = { version = "0.12.24", features = ["json", "stream", "blocking"] } +futures-util = "0.3.30" tauri = { version = "2.9.2", features = [ "macos-private-api", "tray-icon" ] } diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs index 79ad95e..72ddf1d 100644 --- a/src-tauri/src/cmds.rs +++ b/src-tauri/src/cmds.rs @@ -1,5 +1,6 @@ use crate::steam; use crate::tool::*; +use crate::tool::updater::{check_update, download_update, install_update, UpdateInfo}; use crate::vdf::preset; use crate::vdf::preset::VideoConfig; use crate::wrap_err; @@ -47,6 +48,11 @@ pub fn kill_game() -> Result { Ok(common::kill("cs2.exe")) } +#[tauri::command] +pub fn check_process_running(process_name: &str) -> Result { + Ok(common::check_process_running(process_name)) +} + #[tauri::command] pub fn kill_steam() -> Result { Ok(common::kill("steam.exe")) @@ -255,3 +261,42 @@ pub fn read_vprof_report(console_log_path: &str) -> Result { Ok(vprof_lines.join("\n")) } + +// 更新相关命令 + +/// 检查更新 +#[tauri::command] +pub async fn check_app_update( + app: tauri::AppHandle, + custom_endpoint: Option, + github_repo: Option, +) -> Result, String> { + let current_version = app.package_info().version.to_string(); + + wrap_err!(check_update( + custom_endpoint.as_deref(), + github_repo.as_deref(), + ¤t_version + ).await) +} + +/// 下载更新 +#[tauri::command] +pub async fn download_app_update( + app: tauri::AppHandle, + download_url: String, +) -> Result { + let path = wrap_err!(download_update( + &app, + &download_url, + None // 可以添加进度回调 + ).await)?; + + Ok(path.to_string_lossy().to_string()) +} + +/// 安装更新 +#[tauri::command] +pub fn install_app_update(installer_path: String) -> Result<(), String> { + wrap_err!(install_update(&installer_path)) +} diff --git a/src-tauri/src/tool/common.rs b/src-tauri/src/tool/common.rs index 5e603ad..4ed09a2 100644 --- a/src-tauri/src/tool/common.rs +++ b/src-tauri/src/tool/common.rs @@ -81,6 +81,40 @@ pub fn open_path(path: &str) -> Result<(), std::io::Error> { Ok(()) } +pub fn check_process_running(name: &str) -> bool { + // 使用tasklist命令检查进程是否存在 + #[cfg(windows)] + { + let output = Command::new("tasklist") + .args(&["/FI", &format!("IMAGENAME eq {}", name)]) + .creation_flags(CREATE_NO_WINDOW) + .output(); + + if let Ok(output) = output { + let stdout = String::from_utf8_lossy(&output.stdout); + // 检查输出中是否包含进程名(排除表头) + stdout.contains(name) && stdout.contains("exe") + } else { + false + } + } + + #[cfg(not(windows))] + { + // 对于非Windows系统,可以使用ps命令 + let output = Command::new("pgrep") + .arg("-f") + .arg(name) + .output(); + + if let Ok(output) = output { + !output.stdout.is_empty() + } else { + false + } + } +} + mod tests { #[test] fn test_open_path() { diff --git a/src-tauri/src/tool/mod.rs b/src-tauri/src/tool/mod.rs index 7d0ddab..b37bff8 100644 --- a/src-tauri/src/tool/mod.rs +++ b/src-tauri/src/tool/mod.rs @@ -1,3 +1,4 @@ pub mod common; pub mod macros; pub mod powerplan; +pub mod updater; diff --git a/src-tauri/src/tool/updater.rs b/src-tauri/src/tool/updater.rs new file mode 100644 index 0000000..3672566 --- /dev/null +++ b/src-tauri/src/tool/updater.rs @@ -0,0 +1,429 @@ +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; +use std::process::Command; +use tauri::path::BaseDirectory; +use tauri::Manager; + +#[cfg(windows)] +use std::os::windows::process::CommandExt; + +#[cfg(windows)] +const CREATE_NO_WINDOW: u32 = 0x08000000; + +/// 更新信息结构 +#[derive(Debug, Clone, Serialize, Deserialize)] +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 { + version: String, + notes: Option, + pub_date: Option, + download_url: String, + signature: 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)] +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 +pub async fn check_update( + custom_endpoint: Option<&str>, + github_repo: Option<&str>, + current_version: &str, +) -> 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) => {} + } + } + } + + // 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)) + .build()?; + + let response = client.get(endpoint).send().await?; + + if !response.status().is_success() { + return Err(anyhow::anyhow!("HTTP 状态码: {}", response.status())); + } + + let text = response.text().await?; + let update_resp: CustomUpdateResponse = serde_json::from_str(&text) + .context("解析自定义更新服务器响应失败")?; + + // 获取平台特定的下载链接 + 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) + } + } + } else { + update_resp.download_url + }; + + Ok(Some(UpdateInfo { + version: update_resp.version, + notes: update_resp.notes, + pub_date: update_resp.pub_date, + download_url, + signature: update_resp.signature, + })) +} + +/// 检查 GitHub Release +async fn check_github_update(repo: &str) -> Result> { + let url = format!("https://api.github.com/repos/{}/releases/latest", repo); + + 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())); + } + + 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 通常不包含签名 + })) +} + +/// 查找适合当前平台的资源文件 +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()); + } + } + } + + #[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()); + } + } + } + } + } + + #[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!("未找到适合当前平台的安装包")) +} + +/// 比较版本号 +/// 返回: 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 == '_') + .collect(); + + let max_len = v1_parts.len().max(v2_parts.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 { + return 1; + } else if v1_part < v2_part { + return -1; + } + } + + 0 +} + +/// 下载更新文件 +pub async fn download_update( + app: &tauri::AppHandle, + download_url: &str, + progress_callback: Option>, +) -> Result { + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(300)) + .build()?; + + let response = client.get(download_url).send().await?; + + if !response.status().is_success() { + return Err(anyhow::anyhow!("下载失败,HTTP 状态码: {}", response.status())); + } + + let total_size = response.content_length().unwrap_or(0); + + // 获取缓存目录 + let cache_dir = app + .path() + .resolve("updates", BaseDirectory::AppCache) + .context("无法获取缓存目录")?; + + fs::create_dir_all(&cache_dir)?; + + // 从 URL 中提取文件名 + let filename = download_url + .split('/') + .last() + .unwrap_or("update") + .split('?') + .next() + .unwrap_or("update"); + + let file_path = cache_dir.join(filename); + + // 下载文件 + let mut file = fs::File::create(&file_path)?; + let mut stream = response.bytes_stream(); + let mut downloaded: u64 = 0; + + use futures_util::StreamExt; + use std::io::Write; + + while let Some(item) = stream.next().await { + let chunk = item?; + file.write_all(&chunk)?; + downloaded += chunk.len() as u64; + + if let Some(ref callback) = progress_callback { + callback(downloaded, total_size); + } + } + + file.sync_all()?; + + Ok(file_path) +} + +/// 安装更新(Windows NSIS) +#[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.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()?; + + 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/src/vdf/preset.rs b/src-tauri/src/vdf/preset.rs index 902418b..c82c029 100644 --- a/src-tauri/src/vdf/preset.rs +++ b/src-tauri/src/vdf/preset.rs @@ -7,7 +7,9 @@ use serde_json::Value; use std::collections::HashMap; use std::fs; use std::path::Path; -use tauri_plugin_http::reqwest::blocking::get; +use tauri_plugin_http::reqwest:: + +blocking::get; use walkdir::WalkDir; use crate::steam; diff --git a/src/app/(main)/dynamic/page.tsx b/src/app/(main)/dynamic/page.tsx index a799991..53338bb 100644 --- a/src/app/(main)/dynamic/page.tsx +++ b/src/app/(main)/dynamic/page.tsx @@ -1,13 +1,18 @@ "use client" +import { useState } from "react" import { MarkdownRender } from "@/components/markdown" -import { Card, CardBody, CardHeader, CardIcon } from "@/components/window/Card" +import { Card, CardBody, CardHeader, CardIcon, CardTool } from "@/components/window/Card" import { createClient } from "@/utils/supabase/client" import { NewspaperFolding } from "@icon-park/react" import useSWR from "swr" -import { Chip, Skeleton } from "@heroui/react" +import { Chip, Skeleton, Tabs, Tab } from "@heroui/react" +import { Key } from "@react-types/shared" export default function Page() { + const [selectedKey, setSelectedKey] = useState("stable") + const showTestVersions = selectedKey === "test" + return (
@@ -15,68 +20,107 @@ export default function Page() { 动态 - {/* - {}}>读取 - */} + + + + + + - +
) } -const ReleaseNotes = () => { +const ReleaseNotes = ({ showTestVersions }: { showTestVersions: boolean }) => { const noticeFetcher = async () => { const supabase = createClient() - const { data /* , error */ } = await supabase + let query = supabase .from("ReleaseNote") - .select("version, content, created_at") - .eq("stable", true) + .select("version, content, created_at, stable") + + if (!showTestVersions) { + query = query.eq("stable", true) + } + + const { data /* , error */ } = await query .order("created_at", { ascending: false }) .range(0, 10) return data } - const { data: releases /* , error */, isLoading } = useSWR("/api/release-notes", noticeFetcher) - - if (isLoading) return ( -
- - - -
+ const { data: releases /* , error */, isLoading } = useSWR( + `/api/release-notes?test=${showTestVersions}`, + noticeFetcher ) return ( -
    - {releases?.map((release, index) => ( -
  • - {/* */} - - -

    CS工具箱 {release.version}

    - - {/* - 版本:{release?.version} - */} - - 发布时间: - {release.created_at ? new Date(release.created_at as string).toLocaleString() : "未知时间"} - - -
    - -
    - {release.content || "无内容"} -
    -
    -
    - {/* */} -
  • - ))} +
      + {isLoading ? ( + <> +
    • +
    • +
    • + + ) : ( + releases?.map((release) => { + const isStable = release.stable === true + return ( +
    • + {/* */} + + +
      +

      CS工具箱 {release.version}

      + + {isStable ? "正式版" : "测试版"} + +
      + + + 发布时间: + {release.created_at ? new Date(release.created_at as string).toLocaleString() : "未知时间"} + + +
      + +
      + {release.content || "无内容"} +
      +
      +
      + {/* */} +
    • + ) + }) + )}
    ) } diff --git a/src/app/(main)/preference/general/page.tsx b/src/app/(main)/preference/general/page.tsx index 6dad3f7..d125eec 100644 --- a/src/app/(main)/preference/general/page.tsx +++ b/src/app/(main)/preference/general/page.tsx @@ -1,37 +1,53 @@ "use client" import { useAppStore } from "@/store/app" import { Switch } from "@heroui/react" +import { UpdateChecker } from "@/components/cstb/UpdateChecker" export default function Page() { const app = useAppStore() + // 从环境变量或配置中获取更新服务器地址 + // 这里可以改为从 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.setAutoStart(e.target.checked)} - > - 开机自启动 {app.state.autoStart ? "开" : "关"} - - app.setStartHidden(e.target.checked)} - > - 静默启动 {app.state.startHidden ? "开" : "关"} - - {/* hiddenOnClose */} - app.setHiddenOnClose(e.target.checked)} - > - 关闭时最小化到托盘 {app.state.hiddenOnClose ? "开" : "关"} - +
    +
    +

    版本号:{app.state.version}

    +

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

    +

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

    +
    + +
    +

    更新检查

    + +
    + +
    +

    启动设置

    + app.setAutoStart(e.target.checked)} + > + 开机自启动 {app.state.autoStart ? "开" : "关"} + + app.setStartHidden(e.target.checked)} + > + 静默启动 {app.state.startHidden ? "开" : "关"} + + app.setHiddenOnClose(e.target.checked)} + > + 关闭时最小化到托盘 {app.state.hiddenOnClose ? "开" : "关"} + +
    ) } diff --git a/src/app/providers.tsx b/src/app/providers.tsx index 572b6c2..7147359 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -4,6 +4,7 @@ import { ToastProvider } from "@heroui/toast" import { platform } from "@tauri-apps/plugin-os" import { ThemeProvider as NextThemesProvider } from "next-themes" import { useEffect, useState } from "react" +import { AuthProvider } from "@/components/auth/AuthProvider" export default function Providers({ children }: { children: React.ReactNode }) { const [os, setOs] = useState("windows") @@ -17,7 +18,7 @@ export default function Providers({ children }: { children: React.ReactNode }) { > - {children} + {children} ) diff --git a/src/components/cstb/FpsTest.tsx b/src/components/cstb/FpsTest.tsx index 314e356..d8fe9ad 100644 --- a/src/components/cstb/FpsTest.tsx +++ b/src/components/cstb/FpsTest.tsx @@ -11,7 +11,7 @@ const BENCHMARK_MAPS = [ name: "de_dust2_benchmark", workshopId: "3240880604", map: "de_dust2_benchmark", - label: "Dust2 Benchmark", + label: "Dust2", }, { name: "de_ancient", diff --git a/src/components/cstb/UpdateChecker.tsx b/src/components/cstb/UpdateChecker.tsx new file mode 100644 index 0000000..5097320 --- /dev/null +++ b/src/components/cstb/UpdateChecker.tsx @@ -0,0 +1,238 @@ +"use client" + +import { useState } from "react" +import { Button, Progress, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/react" +import { Download, Refresh, CheckCircle } from "@icon-park/react" +import { invoke } from "@tauri-apps/api/core" +import { relaunch } from "@tauri-apps/api/process" +import { addToast } from "@heroui/react" +import { useAppStore } from "@/store/app" + +interface UpdateInfo { + version: string + notes?: string + pub_date?: string + download_url: string + signature?: string +} + +interface UpdateCheckerProps { + customEndpoint?: string + githubRepo?: string +} + +export function UpdateChecker({ customEndpoint, githubRepo }: UpdateCheckerProps) { + const app = useAppStore() + const [checking, setChecking] = useState(false) + const [downloading, setDownloading] = useState(false) + const [updateInfo, setUpdateInfo] = useState(null) + const [downloadProgress, setDownloadProgress] = useState(0) + const [installerPath, setInstallerPath] = useState(null) + const { isOpen, onOpen, onOpenChange } = useDisclosure() + + // 检查更新 + const handleCheckUpdate = async () => { + setChecking(true) + setUpdateInfo(null) + setDownloadProgress(0) + setInstallerPath(null) + + try { + const result = await invoke("check_app_update", { + customEndpoint: customEndpoint || null, + githubRepo: githubRepo || null, + }) + + if (result) { + setUpdateInfo(result) + app.setHasUpdate(true) + onOpen() + addToast({ + title: "发现新版本", + description: `版本 ${result.version} 可用`, + color: "success", + }) + } else { + app.setHasUpdate(false) + addToast({ + title: "已是最新版本", + color: "default", + }) + } + } catch (error) { + console.error("检查更新失败:", error) + addToast({ + title: "检查更新失败", + description: String(error), + color: "danger", + }) + } finally { + setChecking(false) + } + } + + // 下载更新 + const handleDownloadUpdate = async () => { + if (!updateInfo) return + + setDownloading(true) + setDownloadProgress(0) + + try { + // 注意:这里没有实现进度回调,实际项目中可以使用事件监听 + const path = await invoke("download_app_update", { + downloadUrl: updateInfo.download_url, + }) + + setInstallerPath(path) + setDownloadProgress(100) + addToast({ + title: "下载完成", + description: "准备安装更新", + color: "success", + }) + } catch (error) { + console.error("下载更新失败:", error) + addToast({ + title: "下载失败", + description: String(error), + color: "danger", + }) + setDownloadProgress(0) + } finally { + setDownloading(false) + } + } + + // 安装更新 + const handleInstallUpdate = async () => { + if (!installerPath) return + + try { + await invoke("install_app_update", { + installerPath: installerPath, + }) + + addToast({ + title: "安装已启动", + description: "应用将在安装完成后重启", + color: "success", + }) + + // 等待一小段时间后重启 + setTimeout(async () => { + await relaunch() + }, 1000) + } catch (error) { + console.error("安装更新失败:", error) + addToast({ + title: "安装失败", + description: String(error), + color: "danger", + }) + } + } + + // 格式化更新说明(Markdown 转 HTML) + const formatNotes = (notes?: string) => { + if (!notes) return "无更新说明" + // 简单的 Markdown 处理:换行 + return notes.split("\n").map((line, i) => ( + + {line} +
    +
    + )) + } + + return ( +
    +
    + + {app.state.hasUpdate && ( + + + 有新版本可用 + + )} +
    + + {downloading && ( +
    + +

    正在下载更新...

    +
    + )} + + + + {(onClose) => ( + <> + +
    + 发现新版本 + v{updateInfo?.version} +
    +
    + +
    +
    +

    更新说明:

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

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

    + )} +
    +
    + + + {!downloading && !installerPath && ( + + )} + {installerPath && ( + + )} + + + )} +
    +
    +
    + ) +} diff --git a/src/components/cstb/VideoSetting.tsx b/src/components/cstb/VideoSetting.tsx index 252f99a..376d5c4 100644 --- a/src/components/cstb/VideoSetting.tsx +++ b/src/components/cstb/VideoSetting.tsx @@ -1,18 +1,47 @@ import { CloseSmall, Down, Edit, Plus, SettingConfig, Up } from "@icon-park/react" -import { useEffect, useState } from "react" +import { useEffect, useState, useCallback } from "react" import { Card, CardBody, CardHeader, CardIcon, CardTool } from "../window/Card" import { ToolButton } from "../window/ToolButton" -import { addToast, NumberInput, Tab, Tabs, Tooltip } from "@heroui/react" +import { addToast, NumberInput, Tab, Tabs, Tooltip, Chip } from "@heroui/react" import { motion } from "framer-motion" import { useToolStore, VideoSetting as VideoConfig, VideoSettingTemplate } from "@/store/tool" import { useSteamStore } from "@/store/steam" -import { useDebounce } from "ahooks" +import { useDebounce, useDebounceFn } from "ahooks" +import { invoke } from "@tauri-apps/api/core" const VideoSetting = () => { const [hide, setHide] = useState(false) const [edit, setEdit] = useState(false) + const [isGameRunning, setIsGameRunning] = useState(false) const tool = useToolStore() const steam = useSteamStore() + + // 检测游戏是否运行 + const checkGameRunning = useCallback(async () => { + try { + // 尝试检测cs2.exe进程 + const result = await invoke("check_process_running", { processName: "cs2.exe" }).catch(() => false) + setIsGameRunning(result) + return result + } catch { + // 如果命令不存在,使用简单的检测方法 + setIsGameRunning(false) + return false + } + }, []) + + // 防抖的读取函数 + const { run: debouncedGetVideoConfig } = useDebounceFn( + async () => { + if (steam.state.steamDirValid && steam.currentUser()) { + await tool.getVideoConfig(steam.state.steamDir, steam.currentUser()?.steam_id32 || 0) + addToast({ title: "读取成功" }) + } else { + addToast({ title: "请先选择用户", color: "danger" }) + } + }, + { wait: 500, leading: true, trailing: false } + ) const videoSettings = (video: VideoConfig) => { return [ { @@ -249,6 +278,16 @@ const VideoSetting = () => { const [vconfig, setVconfig] = useState(tool.state.videoSetting) + // 初始化时检测游戏运行状态 + useEffect(() => { + void checkGameRunning() + // 定期检测游戏运行状态 + const interval = setInterval(() => { + void checkGameRunning() + }, 2000) + return () => clearInterval(interval) + }, [checkGameRunning]) + useEffect(() => { if (steam.state.steamDirValid && steam.currentUser()) void tool.getVideoConfig(steam.state.steamDir, steam.currentUser()?.steam_id32 || 0) @@ -270,6 +309,16 @@ const VideoSetting = () => { 视频设置 + {isGameRunning && ( + + 游戏运行中 + + )} {/* {tool.state.VideoSettings.map((option, index) => ( @@ -302,6 +351,16 @@ const VideoSetting = () => { { + // 检查游戏是否运行 + const gameRunning = await checkGameRunning() + if (gameRunning) { + addToast({ + title: "无法应用设置", + description: "检测到游戏正在运行,请先关闭游戏后再应用设置", + color: "warning" + }) + return + } await tool.setVideoConfig( steam.state.steamDir, steam.currentUser()?.steam_id32 || 0, @@ -314,6 +373,7 @@ const VideoSetting = () => { setEdit(false) addToast({ title: "应用设置成功" }) }} + isDisabled={isGameRunning} > 应用 @@ -345,15 +405,7 @@ const VideoSetting = () => { { - if (steam.state.steamDirValid && steam.currentUser()) { - await tool.getVideoConfig( - steam.state.steamDir, - steam.currentUser()?.steam_id32 || 0 - ) - addToast({ title: "读取成功" }) - } else addToast({ title: "请先选择用户", color: "danger" }) - }} + onClick={debouncedGetVideoConfig} > 读取 @@ -380,86 +432,86 @@ const VideoSetting = () => { transition={{ duration: 0.2 }} > -
      -
    • - 分辨率 - - { - const _ = edit - ? setVconfig({ - ...vconfig, - defaultres: value.toString(), - }) - : tool.setVideoSetting({ - ...tool.state.videoSetting, - defaultres: value.toString(), - }) - }} - isDisabled={!edit} - radius="full" - step={10} - className="max-w-28" - classNames={{ inputWrapper: "h-10" }} - /> - { - const _ = edit - ? setVconfig({ - ...vconfig, - defaultresheight: value.toString(), - }) - : tool.setVideoSetting({ - ...tool.state.videoSetting, - defaultresheight: value.toString(), - }) - }} - isDisabled={!edit} - radius="full" - step={10} - className="max-w-28" - classNames={{ inputWrapper: "h-10" }} - /> - -
    • - {videoSettings(edit ? vconfig : tool.state.videoSetting).map((vid, index) => ( -
    • - {vid.title} - { - // console.log(vid.type, key) - // 修改 vconfig 名为 vid.type 的 value为 key - const _ = - edit && key - ? setVconfig({ - ...vconfig, - [vid.type]: vid.mapping(key.toString()), - }) - : null - }} - > - {vid.options.map((opt, _) => ( - - ))} - + {edit ? ( + // 编辑状态:显示完整的可编辑控件 +
        +
      • + 分辨率 + + { + setVconfig({ + ...vconfig, + defaultres: value.toString(), + }) + }} + radius="full" + step={10} + className="max-w-28" + classNames={{ inputWrapper: "h-10" }} + /> + { + setVconfig({ + ...vconfig, + defaultresheight: value.toString(), + }) + }} + radius="full" + step={10} + className="max-w-28" + classNames={{ inputWrapper: "h-10" }} + /> +
      • - ))} -
      + {videoSettings(vconfig).map((vid, index) => ( +
    • + {vid.title} + { + if (key) { + setVconfig({ + ...vconfig, + [vid.type]: vid.mapping(key.toString()), + }) + } + }} + > + {vid.options.map((opt, _) => ( + + ))} + +
    • + ))} +
    + ) : ( + // 非编辑状态:显示精简的只读信息 +
    +
    +
    + 分辨率 + + {tool.state.videoSetting.defaultres} × {tool.state.videoSetting.defaultresheight} + +
    + {videoSettings(tool.state.videoSetting).map((vid, index) => ( +
    + {vid.title} + {vid.value} +
    + ))} +
    +
    + )}
    )} diff --git a/src/components/window/Nav.tsx b/src/components/window/Nav.tsx index a17f0f8..eb50519 100644 --- a/src/components/window/Nav.tsx +++ b/src/components/window/Nav.tsx @@ -21,6 +21,7 @@ import { saveAllNow } from "@tauri-store/valtio" import { useSteamStore } from "@/store/steam" import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/react" import { window } from "@tauri-apps/api" +import { AuthButton } from "@/components/auth/AuthButton" const Nav = () => { const { theme, setTheme } = useTheme() @@ -91,6 +92,8 @@ const Nav = () => { + + {/* { platform() === "windows" && ( */} @@ -182,4 +185,8 @@ function ResetModal() { ) } +function AuthButtonWrapper() { + return +} + export default Nav diff --git a/src/components/window/SideBar.tsx b/src/components/window/SideBar.tsx index 1f9d742..caa0869 100644 --- a/src/components/window/SideBar.tsx +++ b/src/components/window/SideBar.tsx @@ -1,5 +1,5 @@ "use client" -import { cn } from "@heroui/react" +import { cn, Tooltip } from "@heroui/react" import { Home, MonitorOne, Movie, NewspaperFolding, Setting, Terminal, Toolkit } from "@icon-park/react" import { usePathname, useRouter } from "next/navigation" import type { ReactNode } from "react" @@ -9,6 +9,17 @@ import { getVersion } from "@tauri-apps/api/app" import { useAppStore } from "@/store/app" import { useSteamStore } from "@/store/steam" +// 路由到页面名称的映射 +const routeNames: Record = { + "/home": "首页", + "/dynamic": "动态", + "/tool": "工具", + "/console": "控制台", + "/gear": "硬件外设", + "/movie": "录像", + "/preference": "偏好设置", +} + interface SideButtonProps { route: string className?: string @@ -23,26 +34,29 @@ const SideButton = ({ }: SideButtonProps & React.ButtonHTMLAttributes) => { const router = useRouter() const path = usePathname() + const pageName = routeNames[route] || route return ( - + {...rest} + > + {children} +
    + + ) } diff --git a/src/store/index.ts b/src/store/index.ts index 3480e6a..9f1bb09 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,12 +1,14 @@ import { appConfigDir } from "@tauri-apps/api/path" import { commands } from "@tauri-store/shared" import { appStore } from "./app" +import { authStore } from "./auth" import { steamStore } from "./steam" import { toolStore } from "./tool" import path from "path" export async function init() { await appStore.start() + await authStore.start() await toolStore.start() await steamStore.start() const appConfigDirPath = await appConfigDir()