[feat] better installer + changelog test version switch + better video config view
This commit is contained in:
248
UPDATE_API.md
Normal file
248
UPDATE_API.md
Normal 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
181
UPDATE_USAGE.md
Normal 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
2
next-env.d.ts
vendored
@@ -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
1
src-tauri/Cargo.lock
generated
@@ -8,6 +8,7 @@ version = "0.0.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"futures-util",
|
||||
"log",
|
||||
"regex",
|
||||
"reqwest",
|
||||
|
||||
@@ -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"
|
||||
] }
|
||||
|
||||
@@ -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(),
|
||||
¤t_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))
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod common;
|
||||
pub mod macros;
|
||||
pub mod powerplan;
|
||||
pub mod updater;
|
||||
|
||||
429
src-tauri/src/tool/updater.rs
Normal file
429
src-tauri/src/tool/updater.rs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,68 +20,107 @@ 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}>
|
||||
{/* <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">
|
||||
<CardHeader>
|
||||
<h3 className="w-full text-2xl font-semibold">CS工具箱 {release.version}</h3>
|
||||
<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() : "未知时间"}
|
||||
</Chip>
|
||||
</span>
|
||||
</CardHeader>
|
||||
<CardBody className="gap-3">
|
||||
<div className="">
|
||||
<MarkdownRender>{release.content || "无内容"}</MarkdownRender>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/* </Link> */}
|
||||
</li>
|
||||
))}
|
||||
<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"
|
||||
>
|
||||
<CardHeader>
|
||||
<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 dark:bg-white/10">
|
||||
发布时间:
|
||||
{release.created_at ? new Date(release.created_at as string).toLocaleString() : "未知时间"}
|
||||
</Chip>
|
||||
</span>
|
||||
</CardHeader>
|
||||
<CardBody className="gap-3">
|
||||
<div className="">
|
||||
<MarkdownRender>{release.content || "无内容"}</MarkdownRender>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/* </Link> */}
|
||||
</li>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<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>
|
||||
<Switch
|
||||
isSelected={app.state.autoStart}
|
||||
size="sm"
|
||||
onChange={(e) => app.setAutoStart(e.target.checked)}
|
||||
>
|
||||
开机自启动 {app.state.autoStart ? "开" : "关"}
|
||||
</Switch>
|
||||
<Switch
|
||||
isSelected={app.state.startHidden}
|
||||
size="sm"
|
||||
onChange={(e) => app.setStartHidden(e.target.checked)}
|
||||
>
|
||||
静默启动 {app.state.startHidden ? "开" : "关"}
|
||||
</Switch>
|
||||
{/* hiddenOnClose */}
|
||||
<Switch
|
||||
isSelected={app.state.hiddenOnClose}
|
||||
size="sm"
|
||||
onChange={(e) => app.setHiddenOnClose(e.target.checked)}
|
||||
>
|
||||
关闭时最小化到托盘 {app.state.hiddenOnClose ? "开" : "关"}
|
||||
</Switch>
|
||||
<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"
|
||||
onChange={(e) => app.setAutoStart(e.target.checked)}
|
||||
>
|
||||
开机自启动 {app.state.autoStart ? "开" : "关"}
|
||||
</Switch>
|
||||
<Switch
|
||||
isSelected={app.state.startHidden}
|
||||
size="sm"
|
||||
onChange={(e) => app.setStartHidden(e.target.checked)}
|
||||
>
|
||||
静默启动 {app.state.startHidden ? "开" : "关"}
|
||||
</Switch>
|
||||
<Switch
|
||||
isSelected={app.state.hiddenOnClose}
|
||||
size="sm"
|
||||
onChange={(e) => app.setHiddenOnClose(e.target.checked)}
|
||||
>
|
||||
关闭时最小化到托盘 {app.state.hiddenOnClose ? "开" : "关"}
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
238
src/components/cstb/UpdateChecker.tsx
Normal file
238
src/components/cstb/UpdateChecker.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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,86 +432,86 @@ const VideoSetting = () => {
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<CardBody>
|
||||
<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
|
||||
)}
|
||||
onValueChange={(value) => {
|
||||
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" }}
|
||||
/>
|
||||
<NumberInput
|
||||
aria-label="height"
|
||||
value={parseInt(
|
||||
edit ? vconfig.defaultresheight : tool.state.videoSetting.defaultresheight,
|
||||
10
|
||||
)}
|
||||
onValueChange={(value) => {
|
||||
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" }}
|
||||
/>
|
||||
</span>
|
||||
</li>
|
||||
{videoSettings(edit ? vconfig : tool.state.videoSetting).map((vid, index) => (
|
||||
<li className="flex flex-col gap-1.5" key={index}>
|
||||
<span className="ml-2">{vid.title}</span>
|
||||
<Tabs
|
||||
size="md"
|
||||
radius="full"
|
||||
className="min-w-36"
|
||||
fullWidth
|
||||
selectedKey={vid.value}
|
||||
onSelectionChange={(key) => {
|
||||
// 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, _) => (
|
||||
<Tab key={opt} title={opt} titleValue={opt} />
|
||||
))}
|
||||
</Tabs>
|
||||
{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(vconfig.defaultres, 10)}
|
||||
onValueChange={(value) => {
|
||||
setVconfig({
|
||||
...vconfig,
|
||||
defaultres: value.toString(),
|
||||
})
|
||||
}}
|
||||
radius="full"
|
||||
step={10}
|
||||
className="max-w-28"
|
||||
classNames={{ inputWrapper: "h-10" }}
|
||||
/>
|
||||
<NumberInput
|
||||
aria-label="height"
|
||||
value={parseInt(vconfig.defaultresheight, 10)}
|
||||
onValueChange={(value) => {
|
||||
setVconfig({
|
||||
...vconfig,
|
||||
defaultresheight: value.toString(),
|
||||
})
|
||||
}}
|
||||
radius="full"
|
||||
step={10}
|
||||
className="max-w-28"
|
||||
classNames={{ inputWrapper: "h-10" }}
|
||||
/>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{videoSettings(vconfig).map((vid, index) => (
|
||||
<li className="flex flex-col gap-1.5" key={index}>
|
||||
<span className="ml-2">{vid.title}</span>
|
||||
<Tabs
|
||||
size="md"
|
||||
radius="full"
|
||||
className="min-w-36"
|
||||
fullWidth
|
||||
selectedKey={vid.value}
|
||||
onSelectionChange={(key) => {
|
||||
if (key) {
|
||||
setVconfig({
|
||||
...vconfig,
|
||||
[vid.type]: vid.mapping(key.toString()),
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
{vid.options.map((opt, _) => (
|
||||
<Tab key={opt} title={opt} titleValue={opt} />
|
||||
))}
|
||||
</Tabs>
|
||||
</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>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,26 +34,29 @@ const SideButton = ({
|
||||
}: SideButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
const router = useRouter()
|
||||
const path = usePathname()
|
||||
const pageName = routeNames[route] || route
|
||||
|
||||
return (
|
||||
<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",
|
||||
path.startsWith(route) && "bg-black/5 dark:bg-white/5"
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
<div
|
||||
<Tooltip content={pageName} showArrow={true} delay={300} placement="right">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push(route || "/")}
|
||||
className={cn(
|
||||
path.startsWith(route) && "opacity-100",
|
||||
"transition-opacity duration-300 opacity-0 h-3.5 w-0.5 absolute left-0.5 bg-purple-500 rounded-full top-1/2 -translate-y-1/2"
|
||||
className,
|
||||
"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"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
<div
|
||||
className={cn(
|
||||
path.startsWith(route) && "opacity-100",
|
||||
"transition-opacity duration-300 opacity-0 h-3.5 w-0.5 absolute left-0.5 bg-purple-500 rounded-full top-1/2 -translate-y-1/2"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user