[feat] better installer + changelog test version switch + better video config view

This commit is contained in:
2025-11-05 11:19:43 +08:00
parent ea0a42dc43
commit 41008cf13c
19 changed files with 1499 additions and 183 deletions

248
UPDATE_API.md Normal file
View File

@@ -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 响应。

181
UPDATE_USAGE.md Normal file
View File

@@ -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 框架文档

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
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.

1
src-tauri/Cargo.lock generated
View File

@@ -8,6 +8,7 @@ version = "0.0.6"
dependencies = [
"anyhow",
"base64 0.22.1",
"futures-util",
"log",
"regex",
"reqwest",

View File

@@ -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"
] }

View File

@@ -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<String, String> {
Ok(common::kill("cs2.exe"))
}
#[tauri::command]
pub fn check_process_running(process_name: &str) -> Result<bool, String> {
Ok(common::check_process_running(process_name))
}
#[tauri::command]
pub fn kill_steam() -> Result<String, String> {
Ok(common::kill("steam.exe"))
@@ -255,3 +261,42 @@ pub fn read_vprof_report(console_log_path: &str) -> Result<String, String> {
Ok(vprof_lines.join("\n"))
}
// 更新相关命令
/// 检查更新
#[tauri::command]
pub async fn check_app_update(
app: tauri::AppHandle,
custom_endpoint: Option<String>,
github_repo: Option<String>,
) -> Result<Option<UpdateInfo>, String> {
let current_version = app.package_info().version.to_string();
wrap_err!(check_update(
custom_endpoint.as_deref(),
github_repo.as_deref(),
&current_version
).await)
}
/// 下载更新
#[tauri::command]
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)?;
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))
}

View File

@@ -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() {

View File

@@ -1,3 +1,4 @@
pub mod common;
pub mod macros;
pub mod powerplan;
pub mod updater;

View File

@@ -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<String>,
pub pub_date: Option<String>,
pub download_url: String,
pub signature: Option<String>,
}
/// 自定义更新服务器响应格式
#[derive(Debug, Deserialize)]
struct CustomUpdateResponse {
version: String,
notes: Option<String>,
pub_date: Option<String>,
download_url: String,
signature: Option<String>,
platforms: Option<PlatformDownloads>,
}
/// 平台特定的下载链接
#[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)]
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
pub async fn check_update(
custom_endpoint: Option<&str>,
github_repo: Option<&str>,
current_version: &str,
) -> 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) => {}
}
}
}
// 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))
.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<Option<UpdateInfo>> {
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<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());
}
}
}
#[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::<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 {
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<Box<dyn Fn(u64, u64) + Send + Sync>>,
) -> Result<PathBuf> {
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
}
}

View File

@@ -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;

View File

@@ -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<Key>("stable")
const showTestVersions = selectedKey === "test"
return (
<section className="flex flex-col gap-4 overflow-hidden">
<Card className="overflow-hidden">
@@ -15,53 +20,90 @@ export default function Page() {
<CardIcon>
<NewspaperFolding />
</CardIcon>
{/* <CardTool>
<ToolButton onClick={async () => {}}>读取</ToolButton>
</CardTool> */}
<CardTool>
<Tabs
selectedKey={selectedKey}
onSelectionChange={setSelectedKey}
size="sm"
radius="full"
classNames={{
base: "min-w-0",
tabList: "gap-1",
tab: "min-w-0 px-3",
tabContent: "text-xs",
}}
>
<Tab key="stable" title="正式版" />
<Tab key="test" title="测试版" />
</Tabs>
</CardTool>
</CardHeader>
<CardBody className="overflow-y-hidden">
<ReleaseNotes />
<ReleaseNotes showTestVersions={showTestVersions} />
</CardBody>
</Card>
</section>
)
}
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 (
<div className="grid h-full grid-cols-1 gap-2 overflow-y-auto rounded-lg grid-flow-dense lg:grid-cols-2 xl:grid-cols-3 pb-9 hide-scrollbar">
<Skeleton className="h-32 rounded-lg"></Skeleton>
<Skeleton className="h-32 rounded-lg"></Skeleton>
<Skeleton className="h-32 rounded-lg"></Skeleton>
</div>
const { data: releases /* , error */, isLoading } = useSWR(
`/api/release-notes?test=${showTestVersions}`,
noticeFetcher
)
return (
<ul className="grid h-full grid-cols-1 gap-2 overflow-y-auto rounded-lg grid-flow-dense lg:grid-cols-2 xl:grid-cols-3 pb-9 hide-scrollbar">
{releases?.map((release, index) => (
<li key={index}>
<ul
className="grid h-full grid-cols-1 gap-2 overflow-y-auto rounded-lg grid-flow-dense lg:grid-cols-2 xl:grid-cols-3 pb-9 hide-scrollbar"
>
{isLoading ? (
<>
<li><Skeleton className="h-32 rounded-lg"></Skeleton></li>
<li><Skeleton className="h-32 rounded-lg"></Skeleton></li>
<li><Skeleton className="h-32 rounded-lg"></Skeleton></li>
</>
) : (
releases?.map((release) => {
const isStable = release.stable === true
return (
<li key={release.version}>
{/* <Link href={`/releases/${release.version}`} className="w-full"> */}
<Card className="w-full h-full pt-3 transition bg-white/60 text-zinc-900 dark:bg-white/5 dark:text-white">
<Card
className="w-full h-full pt-3 transition bg-white/60 text-zinc-900 dark:bg-white/5 dark:text-white"
>
<CardHeader>
<h3 className="w-full text-2xl font-semibold">CS工具箱 {release.version}</h3>
<div className="flex items-center justify-between w-full gap-2">
<h3 className="text-2xl font-semibold">CS工具箱 {release.version}</h3>
<Chip
size="sm"
variant="bordered"
className={
isStable
? "border-blue-400 text-blue-600 dark:border-blue-500 dark:text-blue-400"
: "border-orange-400 text-orange-600 dark:border-orange-500 dark:text-orange-400"
}
>
{isStable ? "正式版" : "测试版"}
</Chip>
</div>
<span className="flex items-center gap-3">
{/* <Chip size="sm" className="bg-zinc-200">
版本:{release?.version}
</Chip> */}
<Chip size="sm" className="bg-zinc-200 dark:bg-white/10">
{release.created_at ? new Date(release.created_at as string).toLocaleString() : "未知时间"}
@@ -76,7 +118,9 @@ const ReleaseNotes = () => {
</Card>
{/* </Link> */}
</li>
))}
)
})
)}
</ul>
)
}

View File

@@ -1,15 +1,31 @@
"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 (
<div className="flex flex-col items-start gap-3 pt-2 pb-1">
<p>{app.state.version}</p>
<p>{app.state.hasUpdate ? "有" : "无"}</p>
<p>使{app.state.useMirror ? "" : ""}</p>
<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>
<div className="w-full border-t border-zinc-200 dark:border-zinc-800 pt-4">
<h3 className="text-sm font-semibold mb-3"></h3>
<UpdateChecker customEndpoint={customEndpoint} githubRepo={githubRepo} />
</div>
<div className="w-full border-t border-zinc-200 dark:border-zinc-800 pt-4 space-y-3">
<h3 className="text-sm font-semibold mb-3"></h3>
<Switch
isSelected={app.state.autoStart}
size="sm"
@@ -24,7 +40,6 @@ export default function Page() {
>
{app.state.startHidden ? "开" : "关"}
</Switch>
{/* hiddenOnClose */}
<Switch
isSelected={app.state.hiddenOnClose}
size="sm"
@@ -33,5 +48,6 @@ export default function Page() {
{app.state.hiddenOnClose ? "开" : "关"}
</Switch>
</div>
</div>
)
}

View File

@@ -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 }) {
>
<NextThemesProvider attribute="class" defaultTheme="light">
<ToastProvider toastOffset={10} placement="top-center" toastProps={{ timeout: 3000 }} />
{children}
<AuthProvider>{children}</AuthProvider>
</NextThemesProvider>
</HeroUIProvider>
)

View File

@@ -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",

View File

@@ -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<UpdateInfo | null>(null)
const [downloadProgress, setDownloadProgress] = useState(0)
const [installerPath, setInstallerPath] = useState<string | null>(null)
const { isOpen, onOpen, onOpenChange } = useDisclosure()
// 检查更新
const handleCheckUpdate = async () => {
setChecking(true)
setUpdateInfo(null)
setDownloadProgress(0)
setInstallerPath(null)
try {
const result = await invoke<UpdateInfo | null>("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<string>("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) => (
<span key={i}>
{line}
<br />
</span>
))
}
return (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<Button
size="sm"
color="primary"
variant="flat"
startContent={checking ? undefined : <Refresh />}
isLoading={checking}
onPress={handleCheckUpdate}
>
{checking ? "检查中..." : "检查更新"}
</Button>
{app.state.hasUpdate && (
<span className="text-sm text-green-600 dark:text-green-400 flex items-center gap-1">
<CheckCircle size={16} />
</span>
)}
</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">
<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>
</ModalHeader>
<ModalBody>
<div className="space-y-3">
<div>
<p className="text-sm font-semibold mb-1"></p>
<div className="text-sm text-zinc-600 dark:text-zinc-400 whitespace-pre-wrap">
{formatNotes(updateInfo?.notes)}
</div>
</div>
{updateInfo?.pub_date && (
<p className="text-xs text-zinc-500">{new Date(updateInfo.pub_date).toLocaleString("zh-CN")}</p>
)}
</div>
</ModalBody>
<ModalFooter>
<Button color="danger" variant="light" 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>
</>
)}
</ModalContent>
</Modal>
</div>
)
}

View File

@@ -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<boolean>("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<VideoConfig>(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 = () => {
<CardHeader>
<CardIcon>
<SettingConfig />
{isGameRunning && (
<Chip
size="sm"
color="warning"
variant="flat"
className="ml-2"
>
</Chip>
)}
</CardIcon>
<CardTool>
{/* {tool.state.VideoSettings.map((option, index) => (
@@ -302,6 +351,16 @@ const VideoSetting = () => {
</ToolButton>
<ToolButton
onClick={async () => {
// 检查游戏是否运行
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}
>
<Plus />
@@ -345,15 +405,7 @@ const VideoSetting = () => {
</ToolButton>
<ToolButton
onClick={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" })
}}
onClick={debouncedGetVideoConfig}
>
</ToolButton>
@@ -380,28 +432,21 @@ const VideoSetting = () => {
transition={{ duration: 0.2 }}
>
<CardBody>
{edit ? (
// 编辑状态:显示完整的可编辑控件
<ul className="flex flex-wrap gap-3 mt-1">
<li className="flex flex-col gap-1.5">
<span className="ml-2"></span>
<span className="flex gap-3">
<NumberInput
aria-label="width"
value={parseInt(
edit ? vconfig.defaultres : tool.state.videoSetting.defaultres,
10
)}
value={parseInt(vconfig.defaultres, 10)}
onValueChange={(value) => {
const _ = edit
? setVconfig({
setVconfig({
...vconfig,
defaultres: value.toString(),
})
: tool.setVideoSetting({
...tool.state.videoSetting,
defaultres: value.toString(),
})
}}
isDisabled={!edit}
radius="full"
step={10}
className="max-w-28"
@@ -409,22 +454,13 @@ const VideoSetting = () => {
/>
<NumberInput
aria-label="height"
value={parseInt(
edit ? vconfig.defaultresheight : tool.state.videoSetting.defaultresheight,
10
)}
value={parseInt(vconfig.defaultresheight, 10)}
onValueChange={(value) => {
const _ = edit
? setVconfig({
setVconfig({
...vconfig,
defaultresheight: value.toString(),
})
: tool.setVideoSetting({
...tool.state.videoSetting,
defaultresheight: value.toString(),
})
}}
isDisabled={!edit}
radius="full"
step={10}
className="max-w-28"
@@ -432,7 +468,7 @@ const VideoSetting = () => {
/>
</span>
</li>
{videoSettings(edit ? vconfig : tool.state.videoSetting).map((vid, index) => (
{videoSettings(vconfig).map((vid, index) => (
<li className="flex flex-col gap-1.5" key={index}>
<span className="ml-2">{vid.title}</span>
<Tabs
@@ -442,15 +478,12 @@ const VideoSetting = () => {
fullWidth
selectedKey={vid.value}
onSelectionChange={(key) => {
// console.log(vid.type, key)
// 修改 vconfig 名为 vid.type 的 value为 key
const _ =
edit && key
? setVconfig({
if (key) {
setVconfig({
...vconfig,
[vid.type]: vid.mapping(key.toString()),
})
: null
}
}}
>
{vid.options.map((opt, _) => (
@@ -460,6 +493,25 @@ const VideoSetting = () => {
</li>
))}
</ul>
) : (
// 非编辑状态:显示精简的只读信息
<div className="mt-1">
<div className="grid grid-cols-3 md:grid-cols-4 gap-2.5">
<div className="flex flex-col gap-1">
<span className="text-xs text-default-500"></span>
<span className="text-sm font-medium">
{tool.state.videoSetting.defaultres} × {tool.state.videoSetting.defaultresheight}
</span>
</div>
{videoSettings(tool.state.videoSetting).map((vid, index) => (
<div className="flex flex-col gap-1" key={index}>
<span className="text-xs text-default-500">{vid.title}</span>
<span className="text-sm font-medium">{vid.value}</span>
</div>
))}
</div>
</div>
)}
</CardBody>
</motion.div>
)}

View File

@@ -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 = () => {
</Link>
</Tooltip>
<AuthButtonWrapper />
<ResetModal />
{/* { platform() === "windows" && ( */}
@@ -182,4 +185,8 @@ function ResetModal() {
)
}
function AuthButtonWrapper() {
return <AuthButton />
}
export default Nav

View File

@@ -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<string, string> = {
"/home": "首页",
"/dynamic": "动态",
"/tool": "工具",
"/console": "控制台",
"/gear": "硬件外设",
"/movie": "录像",
"/preference": "偏好设置",
}
interface SideButtonProps {
route: string
className?: string
@@ -23,14 +34,16 @@ const SideButton = ({
}: SideButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
const router = useRouter()
const path = usePathname()
const pageName = routeNames[route] || route
return (
<Tooltip content={pageName} showArrow={true} delay={300} placement="right">
<button
type="button"
onClick={() => router.push(route || "/")}
className={cn(
className,
"p-2.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition relative active:scale-90",
"p-2.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition relative active:scale-90 cursor-pointer",
path.startsWith(route) && "bg-black/5 dark:bg-white/5"
)}
{...rest}
@@ -43,6 +56,7 @@ const SideButton = ({
)}
/>
</button>
</Tooltip>
)
}

View File

@@ -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()