Compare commits
75 Commits
v0.0.5-bet
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
695d246a77 | ||
|
|
42f49d784e | ||
|
|
843fc03ddc | ||
|
|
f7efcb7fc9 | ||
|
|
388c74831e | ||
|
|
0f938f6f3e | ||
|
|
812bc64b6f | ||
|
|
4b7735575a | ||
|
|
5e663dc79e | ||
|
|
cd19faba47 | ||
|
|
4c151c3dd5 | ||
|
|
11afc6dc9e | ||
|
|
c50fedd305 | ||
|
|
6103164e37 | ||
|
|
c8d8339f30 | ||
|
|
41105d3bab | ||
|
|
e824455577 | ||
|
|
e146fbe393 | ||
|
|
e7e0bbd953 | ||
|
|
7e097cc9cc | ||
|
|
8bef96bb02 | ||
|
|
f7c9e455f7 | ||
|
|
9f29857fd3 | ||
|
|
dcac1295c6 | ||
|
|
b7f0d0b0cc | ||
|
|
ae567eece7 | ||
|
|
cce56eaf5e | ||
|
|
e774b31396 | ||
|
|
72eef189da | ||
|
|
8eeb7347a2 | ||
|
|
4c0c33382f | ||
|
|
8550887bfb | ||
|
|
ce410f7a26 | ||
|
|
c19adcc3f8 | ||
|
|
6710fe1754 | ||
|
|
41008cf13c | ||
|
|
ea0a42dc43 | ||
|
|
543c3344d1 | ||
|
|
d6ce9bd5f3 | ||
|
|
ae67e90745 | ||
|
|
67b6c74907 | ||
|
|
a5f5a2a5ff | ||
|
|
84d0cd7473 | ||
|
|
0c306b658e | ||
|
|
44fcd81643 | ||
|
|
35ecf4ce0a | ||
|
|
7012f0d9fe | ||
|
|
529ae547e0 | ||
|
|
d814b0a3fa | ||
|
|
3dc271866f | ||
|
|
c5330aec8f | ||
|
|
c67c358354 | ||
|
|
1d23c29ba8 | ||
|
|
dbedf25f0c | ||
|
|
f9ff43f698 | ||
|
|
a66100bfaf | ||
|
|
5b7f763221 | ||
|
|
4086fa88c2 | ||
|
|
b6dbbf94ec | ||
|
|
bbcaf8e0d1 | ||
|
|
faec03afb1 | ||
|
|
7ec20984c4 | ||
|
|
dabbab9f3e | ||
|
|
0e7e6dd3ba | ||
|
|
a10cf8eddf | ||
|
|
63172f12bc | ||
|
|
114def0b96 | ||
|
|
93cda8dc85 | ||
|
|
afa7355f4d | ||
|
|
e29b48b98c | ||
|
|
21cdc7c32d | ||
|
|
27439593b4 | ||
|
|
ed040aadf5 | ||
|
|
8f885a5412 | ||
|
|
ee03bf0160 |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -38,4 +38,11 @@ yarn-error.log*
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
|
||||||
|
temp/
|
||||||
|
src-tauri/temp/
|
||||||
|
log/
|
||||||
|
.tauri/
|
||||||
|
todo/
|
||||||
|
next-env.d.ts
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
22
|
25
|
||||||
|
|||||||
119
.vscode/tasks.json
vendored
119
.vscode/tasks.json
vendored
@@ -4,28 +4,117 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"label": "Dev Tauri",
|
"label": "🚀 Dev Tauri",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "bun tauri dev",
|
"command": "bun tauri dev",
|
||||||
"problemMatcher": [
|
"group": {
|
||||||
"$vite"
|
"kind": "build",
|
||||||
],
|
"isDefault": false
|
||||||
|
},
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "new"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Build Tauri to nsis installer",
|
"label": "📦 Build: Release (NSIS)",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "bun tauri build -b nsis",
|
"command": "& .\\.tauri\\set-env.ps1; bun tauri build -b nsis",
|
||||||
"problemMatcher": [
|
"group": "build",
|
||||||
"$vite"
|
"presentation": {
|
||||||
],
|
"reveal": "always",
|
||||||
|
"panel": "new"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Build Tauri",
|
"label": "📦 Build: Release (All)",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "bun tauri build",
|
"command": "& .\\.tauri\\set-env.ps1; bun tauri build",
|
||||||
"problemMatcher": [
|
"group": "build",
|
||||||
"$vite"
|
"presentation": {
|
||||||
],
|
"reveal": "always",
|
||||||
|
"panel": "new"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "⚡ Build: Fast (Dev Profile)",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "& .\\.tauri\\set-env.ps1; bun tauri build -b nsis -- --profile dev",
|
||||||
|
"group": "build",
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "new"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "⚡ Build: Fast Prod (Fast-Release Profile)",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "& .\\.tauri\\set-env.ps1; bun tauri build -b nsis -- --profile fast-release",
|
||||||
|
"group": "build",
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "new"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "📦 Build: Release + Rename + Gen Latest",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "& .\\.tauri\\set-env.ps1; bun tauri build -b nsis; if ($LASTEXITCODE -eq 0) { node scripts/generate-latest-json.js --base-url https://github.com/plsgo/cstb/releases --rename }",
|
||||||
|
"group": "build",
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "new"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "⚡ Build: Fast + Rename + Gen Latest",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "& .\\.tauri\\set-env.ps1; bun tauri build -b nsis -- --profile dev; if ($LASTEXITCODE -eq 0) { node scripts/generate-latest-json.js --base-url https://github.com/plsgo/cstb/releases --target debug --rename }",
|
||||||
|
"group": "build",
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "new"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "⚡ Build: Fast Prod + Rename + Gen Latest",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "& .\\.tauri\\set-env.ps1; bun tauri build -b nsis -- --profile fast-release; if ($LASTEXITCODE -eq 0) { node scripts/generate-latest-json.js --base-url https://github.com/plsgo/cstb/releases --target fast-release --rename }",
|
||||||
|
"group": "build",
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "new"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "🔄 Rename Build Artifacts",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "node scripts/rename-build-artifacts.js --target release",
|
||||||
|
"group": "build",
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "new"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "📄 Generate Latest JSON",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "node scripts/generate-latest-json.js --base-url https://github.com/plsgo/cstb/releases",
|
||||||
|
"group": "build",
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "new"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "📄 Generate Latest JSON (with Rename)",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "node scripts/generate-latest-json.js --base-url https://github.com/plsgo/cstb/releases --rename",
|
||||||
|
"group": "build",
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "new"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
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 框架文档
|
||||||
147
docs/auth-integration.md
Normal file
147
docs/auth-integration.md
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
# 认证集成说明
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本应用已实现与网页端(https://cstb.upup.cool/)的邮箱注册登录功能集成。用户可以通过网页端登录/注册,然后通过 deep-link 回调到本地应用。
|
||||||
|
|
||||||
|
## 实现方案
|
||||||
|
|
||||||
|
### 1. 用户流程
|
||||||
|
|
||||||
|
1. 用户在本地应用中点击"登录"或"注册"按钮
|
||||||
|
2. 应用打开浏览器,跳转到网页端登录/注册页面
|
||||||
|
3. 用户在网页端完成登录/注册
|
||||||
|
4. 网页端重定向到 `cstb://auth` deep-link,携带认证信息
|
||||||
|
5. 本地应用接收 deep-link 回调,解析并设置 session
|
||||||
|
6. 用户状态同步到本地应用
|
||||||
|
|
||||||
|
### 2. 技术实现
|
||||||
|
|
||||||
|
#### 前端(Next.js + Tauri)
|
||||||
|
|
||||||
|
- **认证 Store** (`src/store/auth.ts`): 管理用户状态和会话
|
||||||
|
- **认证工具** (`src/utils/auth.ts`): 处理登录/注册跳转和回调解析
|
||||||
|
- **认证 Provider** (`src/components/auth/AuthProvider.tsx`): 监听 deep-link 和认证状态变化
|
||||||
|
- **认证按钮** (`src/components/auth/AuthButton.tsx`): UI 组件,显示登录状态和用户信息
|
||||||
|
|
||||||
|
#### Deep-link 配置
|
||||||
|
|
||||||
|
- Scheme: `cstb`
|
||||||
|
- 回调 URL 格式: `cstb://auth?access_token=xxx&refresh_token=xxx` 或 `cstb://auth?code=xxx`
|
||||||
|
|
||||||
|
### 3. 网页端集成要求
|
||||||
|
|
||||||
|
网页端需要在登录/注册成功后,重定向到 deep-link URL。以下是几种实现方式:
|
||||||
|
|
||||||
|
#### 方式 1: 使用 access_token 和 refresh_token(推荐)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在登录成功后
|
||||||
|
const { data } = await supabase.auth.signInWithPassword({ email, password })
|
||||||
|
|
||||||
|
if (data.session) {
|
||||||
|
const redirectUrl = new URL('cstb://auth', window.location.href)
|
||||||
|
redirectUrl.searchParams.set('access_token', data.session.access_token)
|
||||||
|
redirectUrl.searchParams.set('refresh_token', data.session.refresh_token)
|
||||||
|
window.location.href = redirectUrl.toString()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 方式 2: 使用 PKCE flow(更安全)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在登录页面初始化时
|
||||||
|
const { data } = await supabase.auth.signInWithOAuth({
|
||||||
|
provider: 'email',
|
||||||
|
options: {
|
||||||
|
redirectTo: 'cstb://auth'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理回调
|
||||||
|
const { data: { session } } = await supabase.auth.getSession()
|
||||||
|
if (session) {
|
||||||
|
window.location.href = 'cstb://auth?code=' + session.access_token
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 方式 3: 使用完整 session JSON
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在登录成功后
|
||||||
|
const { data } = await supabase.auth.signInWithPassword({ email, password })
|
||||||
|
|
||||||
|
if (data.session) {
|
||||||
|
const redirectUrl = new URL('cstb://auth', window.location.href)
|
||||||
|
redirectUrl.searchParams.set('session', encodeURIComponent(JSON.stringify(data.session)))
|
||||||
|
window.location.href = redirectUrl.toString()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 网页端修改示例
|
||||||
|
|
||||||
|
在 `https://cstb.upup.cool/auth/login` 和 `https://cstb.upup.cool/auth/signup` 页面中:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 检查是否有 redirect 参数
|
||||||
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
|
const redirectTo = urlParams.get('redirect') // 应该是 'cstb://auth'
|
||||||
|
|
||||||
|
// 登录成功后
|
||||||
|
const handleLogin = async (email: string, password: string) => {
|
||||||
|
const { data, error } = await supabase.auth.signInWithPassword({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
// 处理错误
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.session && redirectTo) {
|
||||||
|
// 重定向到 deep-link
|
||||||
|
const redirectUrl = new URL(redirectTo)
|
||||||
|
redirectUrl.searchParams.set('access_token', data.session.access_token)
|
||||||
|
redirectUrl.searchParams.set('refresh_token', data.session.refresh_token)
|
||||||
|
window.location.href = redirectUrl.toString()
|
||||||
|
} else {
|
||||||
|
// 正常网页端跳转
|
||||||
|
window.location.href = '/'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 环境变量
|
||||||
|
|
||||||
|
确保以下环境变量已配置:
|
||||||
|
|
||||||
|
```env
|
||||||
|
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 测试
|
||||||
|
|
||||||
|
1. 启动应用
|
||||||
|
2. 点击导航栏的用户图标
|
||||||
|
3. 选择"登录"或"注册"
|
||||||
|
4. 在浏览器中完成登录/注册
|
||||||
|
5. 应用应该自动接收回调并显示登录成功提示
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **安全性**: 虽然 access_token 和 refresh_token 通过 URL 传递,但由于 deep-link 是本地协议,相对安全。但建议使用 PKCE flow 以获得更好的安全性。
|
||||||
|
|
||||||
|
2. **错误处理**: 网页端应该处理登录失败的情况,不要重定向到 deep-link。
|
||||||
|
|
||||||
|
3. **用户体验**: 可以在网页端显示"正在跳转到应用..."的提示,提升用户体验。
|
||||||
|
|
||||||
|
4. **兼容性**: 确保 deep-link 在所有目标平台上都已正确注册(Windows/macOS/Linux)。
|
||||||
|
|
||||||
|
## 故障排查
|
||||||
|
|
||||||
|
1. **Deep-link 未触发**: 检查 `tauri.conf.json` 中 deep-link 配置是否正确
|
||||||
|
2. **Session 未保存**: 检查 Supabase client 配置,确保使用 localStorage
|
||||||
|
3. **回调参数错误**: 检查网页端传递的参数格式是否正确
|
||||||
|
|
||||||
271
docs/web-integration-example.md
Normal file
271
docs/web-integration-example.md
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
# 网页端集成示例代码
|
||||||
|
|
||||||
|
本文档提供网页端(https://cstb.upup.cool/)实现登录/注册回调的示例代码。
|
||||||
|
|
||||||
|
## 登录页面示例
|
||||||
|
|
||||||
|
在 `auth/login` 页面中:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { createClient } from '@/utils/supabase/client'
|
||||||
|
import { useSearchParams } from 'next/navigation'
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const redirectTo = searchParams.get('redirect') // 应该是 'cstb://auth'
|
||||||
|
|
||||||
|
const supabase = createClient()
|
||||||
|
|
||||||
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase.auth.signInWithPassword({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
alert(`登录失败: ${error.message}`)
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.session) {
|
||||||
|
// 如果有 redirect 参数,说明是从应用跳转过来的
|
||||||
|
if (redirectTo) {
|
||||||
|
// 构建 deep-link URL
|
||||||
|
const redirectUrl = new URL(redirectTo)
|
||||||
|
redirectUrl.searchParams.set('access_token', data.session.access_token)
|
||||||
|
redirectUrl.searchParams.set('refresh_token', data.session.refresh_token)
|
||||||
|
|
||||||
|
// 重定向到应用
|
||||||
|
window.location.href = redirectUrl.toString()
|
||||||
|
} else {
|
||||||
|
// 正常网页端跳转
|
||||||
|
window.location.href = '/'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error)
|
||||||
|
alert('登录时发生错误')
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleLogin}>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="邮箱"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="密码"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button type="submit" disabled={loading}>
|
||||||
|
{loading ? '登录中...' : '登录'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注册页面示例
|
||||||
|
|
||||||
|
在 `auth/signup` 页面中:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { createClient } from '@/utils/supabase/client'
|
||||||
|
import { useSearchParams } from 'next/navigation'
|
||||||
|
|
||||||
|
export default function SignupPage() {
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const redirectTo = searchParams.get('redirect') // 应该是 'cstb://auth'
|
||||||
|
|
||||||
|
const supabase = createClient()
|
||||||
|
|
||||||
|
const handleSignup = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
alert('两次输入的密码不一致')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase.auth.signUp({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
alert(`注册失败: ${error.message}`)
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.session) {
|
||||||
|
// 如果有 redirect 参数,说明是从应用跳转过来的
|
||||||
|
if (redirectTo) {
|
||||||
|
// 构建 deep-link URL
|
||||||
|
const redirectUrl = new URL(redirectTo)
|
||||||
|
redirectUrl.searchParams.set('access_token', data.session.access_token)
|
||||||
|
redirectUrl.searchParams.set('refresh_token', data.session.refresh_token)
|
||||||
|
|
||||||
|
// 重定向到应用
|
||||||
|
window.location.href = redirectUrl.toString()
|
||||||
|
} else {
|
||||||
|
// 正常网页端跳转
|
||||||
|
alert('注册成功!请检查邮箱验证链接。')
|
||||||
|
window.location.href = '/'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 需要邮箱验证
|
||||||
|
alert('注册成功!请检查邮箱中的验证链接。')
|
||||||
|
if (!redirectTo) {
|
||||||
|
window.location.href = '/'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Signup error:', error)
|
||||||
|
alert('注册时发生错误')
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSignup}>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="邮箱"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="密码"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
placeholder="确认密码"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button type="submit" disabled={loading}>
|
||||||
|
{loading ? '注册中...' : '注册'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用 OAuth 提供商的示例
|
||||||
|
|
||||||
|
如果使用第三方登录(如 Google、GitHub 等):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const handleOAuthLogin = async (provider: 'google' | 'github') => {
|
||||||
|
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||||
|
provider,
|
||||||
|
options: {
|
||||||
|
redirectTo: redirectTo || `${window.location.origin}/auth/callback`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
alert(`登录失败: ${error.message}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuth 会重定向到回调页面,在回调页面中处理
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 回调页面处理
|
||||||
|
|
||||||
|
如果需要处理 OAuth 回调:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// auth/callback/page.tsx
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { createClient } from '@/utils/supabase/client'
|
||||||
|
import { useSearchParams } from 'next/navigation'
|
||||||
|
|
||||||
|
export default function CallbackPage() {
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const redirectTo = searchParams.get('redirect') // 从应用跳转时的原始 redirect
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleCallback = async () => {
|
||||||
|
const supabase = createClient()
|
||||||
|
const { data: { session }, error } = await supabase.auth.getSession()
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error getting session:', error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session) {
|
||||||
|
// 如果是从应用跳转过来的,重定向回应用
|
||||||
|
if (redirectTo) {
|
||||||
|
const redirectUrl = new URL(redirectTo)
|
||||||
|
redirectUrl.searchParams.set('access_token', session.access_token)
|
||||||
|
redirectUrl.searchParams.set('refresh_token', session.refresh_token)
|
||||||
|
window.location.href = redirectUrl.toString()
|
||||||
|
} else {
|
||||||
|
// 正常网页端跳转
|
||||||
|
window.location.href = '/'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleCallback()
|
||||||
|
}, [redirectTo])
|
||||||
|
|
||||||
|
return <div>正在处理登录...</div>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **安全性**: 确保只在 HTTPS 环境下使用,避免 token 泄露
|
||||||
|
2. **错误处理**: 始终处理登录/注册失败的情况
|
||||||
|
3. **用户体验**: 在重定向前显示加载状态
|
||||||
|
4. **验证**: 如果启用了邮箱验证,需要处理未验证的情况
|
||||||
|
|
||||||
|
## 测试步骤
|
||||||
|
|
||||||
|
1. 在应用中使用 `https://cstb.upup.cool/auth/login?redirect=cstb://auth` 打开登录页面
|
||||||
|
2. 完成登录后,应该自动跳转回应用
|
||||||
|
3. 检查应用是否成功接收并设置了 session
|
||||||
|
|
||||||
1
next-env.d.ts
vendored
1
next-env.d.ts
vendored
@@ -1,5 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
85
package.json
85
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cstb-next",
|
"name": "cstb-next",
|
||||||
"version": "0.0.1",
|
"version": "0.0.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Purp1e",
|
"name": "Purp1e",
|
||||||
@@ -12,67 +12,74 @@
|
|||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"build": "tauri build",
|
"build": "tauri build",
|
||||||
"build-fast": "tauri build -b nsis -- --profile dev",
|
"build-fast": "tauri build -b nsis -- --profile dev",
|
||||||
|
"build-fast-prod": "tauri build -b nsis -- --profile fast-release",
|
||||||
"dev": "tauri dev",
|
"dev": "tauri dev",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"fix": "next lint --fix"
|
"fix": "next lint --fix",
|
||||||
|
"gen": "node scripts/generate-latest-json.js --base-url https://github.com/plsgo/cstb/releases --rename",
|
||||||
|
"gen:debug": "node scripts/generate-latest-json.js --base-url https://github.com/plsgo/cstb/releases --target debug --rename",
|
||||||
|
"gen:fast-release": "node scripts/generate-latest-json.js --base-url https://github.com/plsgo/cstb/releases --target fast-release --rename"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formkit/auto-animate": "^0.8.2",
|
"@formkit/auto-animate": "^0.8.4",
|
||||||
"@heroui/react": "^2.7.5",
|
"@heroui/react": "^2.8.5",
|
||||||
"@icon-park/react": "^1.4.2",
|
"@icon-park/react": "^1.4.2",
|
||||||
"@reactuses/core": "6.0.1",
|
"@reactuses/core": "6.0.1",
|
||||||
"@supabase/ssr": "0.6.1",
|
"@supabase/ssr": "0.6.1",
|
||||||
"@tauri-apps/api": "2.4.0",
|
"@tauri-apps/api": "^2.9.0",
|
||||||
"@tauri-apps/plugin-autostart": "^2.2.0",
|
"@tauri-apps/plugin-autostart": "^2.5.1",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "2.2.2",
|
"@tauri-apps/plugin-cli": "^2.4.1",
|
||||||
"@tauri-apps/plugin-deep-link": "~2.2.0",
|
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||||
"@tauri-apps/plugin-dialog": "~2.2.0",
|
"@tauri-apps/plugin-deep-link": "^2.4.5",
|
||||||
"@tauri-apps/plugin-fs": "2.2.0",
|
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||||
"@tauri-apps/plugin-global-shortcut": "2.2.0",
|
"@tauri-apps/plugin-fs": "^2.4.4",
|
||||||
"@tauri-apps/plugin-http": "2.4.2",
|
"@tauri-apps/plugin-global-shortcut": "^2.3.1",
|
||||||
"@tauri-apps/plugin-notification": "2.2.2",
|
"@tauri-apps/plugin-http": "^2.5.4",
|
||||||
"@tauri-apps/plugin-os": "2.2.1",
|
"@tauri-apps/plugin-notification": "^2.3.3",
|
||||||
"@tauri-apps/plugin-process": "2.2.0",
|
"@tauri-apps/plugin-os": "^2.3.2",
|
||||||
"@tauri-apps/plugin-shell": "2.2.0",
|
"@tauri-apps/plugin-process": "^2.3.1",
|
||||||
"@tauri-apps/plugin-store": "^2.2.0",
|
"@tauri-apps/plugin-shell": "^2.3.3",
|
||||||
"@tauri-store/valtio": "2.1.1",
|
"@tauri-apps/plugin-store": "^2.4.1",
|
||||||
|
"@tauri-apps/plugin-updater": "^2.9.0",
|
||||||
|
"@tauri-store/valtio": "^3.2.0",
|
||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"ahooks": "^3.8.4",
|
"ahooks": "^3.9.6",
|
||||||
"framer-motion": "^12.5.0",
|
"framer-motion": "^12.23.24",
|
||||||
"next": "15.2.3",
|
"next": "16.0.1",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.0.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"swr": "^2.3.3",
|
"swr": "^2.3.6",
|
||||||
"tauri-plugin-system-info-api": "^2.0.10"
|
"tauri-plugin-system-info-api": "^2.0.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.4.0",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@tauri-apps/cli": "^2.9.4",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@testing-library/react": "^16.2.0",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.13.11",
|
"@types/node": "^22.19.1",
|
||||||
"@types/react": "19.0.10",
|
"@types/react": "19.0.10",
|
||||||
"@types/react-dom": "19.0.4",
|
"@types/react-dom": "19.0.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.27.0",
|
"@typescript-eslint/eslint-plugin": "^8.47.0",
|
||||||
"@typescript-eslint/parser": "^8.27.0",
|
"@typescript-eslint/parser": "^8.47.0",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.22",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cssnano": "^7.0.6",
|
"cssnano": "^7.1.2",
|
||||||
"eslint": "9.23.0",
|
"eslint": "9.23.0",
|
||||||
"eslint-config-next": "15.2.3",
|
"eslint-config-next": "15.2.3",
|
||||||
"lint-staged": "^15.5.0",
|
"lint-staged": "^15.5.2",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.6",
|
||||||
"postcss-import": "^16.1.0",
|
"postcss-import": "^16.1.1",
|
||||||
"postcss-nesting": "^13.0.1",
|
"postcss-nesting": "^13.0.2",
|
||||||
"tailwind-merge": "3.0.2",
|
"tailwind-merge": "3.0.2",
|
||||||
"tailwindcss": "3.4.17",
|
"tailwindcss": "^4.1.17",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.9.3"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
"postcss-import": {},
|
"@tailwindcss/postcss": {},
|
||||||
"tailwindcss/nesting": "postcss-nesting",
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {}),
|
...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
93
scripts/README.md
Normal file
93
scripts/README.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# 生成 latest.json 脚本使用说明
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
`generate-latest-json.js` 脚本用于生成 Tauri 更新器所需的 `latest.json` 文件。该文件包含版本信息、更新说明、下载链接和签名信息。
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基本用法
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run generate-latest -- --base-url https://your-server.com/releases
|
||||||
|
```
|
||||||
|
|
||||||
|
### 完整示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run generate-latest -- --base-url https://your-server.com/releases --version 0.0.6-beta.6 --notes "修复了已知问题"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 参数说明
|
||||||
|
|
||||||
|
- `--base-url` (必需): 更新文件的基 URL,例如 `https://your-server.com/releases`
|
||||||
|
- `--version` (可选): 版本号,如果不提供,将从 `tauri.conf.json` 读取
|
||||||
|
- `--notes` (可选): 更新说明,支持 Markdown 格式
|
||||||
|
|
||||||
|
## 工作流程
|
||||||
|
|
||||||
|
1. **构建应用**: 首先确保已经构建了应用
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **生成 latest.json**: 运行脚本生成 `latest.json` 文件
|
||||||
|
```bash
|
||||||
|
npm run generate-latest -- --base-url https://your-server.com/releases
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **上传文件**: 将以下文件上传到服务器:
|
||||||
|
- 安装包文件(如 `.exe`, `.dmg`, `.AppImage`)
|
||||||
|
- 签名文件(`.sig`)
|
||||||
|
- `latest.json` 文件
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
生成的 `latest.json` 文件格式:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "0.0.6-beta.6",
|
||||||
|
"notes": "版本 0.0.6-beta.6 更新",
|
||||||
|
"pub_date": "2025-01-15T10:00:00.000Z",
|
||||||
|
"platforms": {
|
||||||
|
"windows-x86_64": {
|
||||||
|
"url": "https://your-server.com/releases/CS工具箱_0.0.6-beta.6_x64-setup.exe",
|
||||||
|
"signature": "签名内容..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **构建产物**: 脚本会自动查找构建产物目录中的文件,确保已经完成构建
|
||||||
|
2. **签名文件**: 如果存在 `.sig` 文件,脚本会自动读取并包含在 `latest.json` 中
|
||||||
|
3. **版本匹配**: 脚本会根据版本号匹配文件,确保文件名包含版本号
|
||||||
|
4. **平台支持**: 脚本支持 Windows、macOS 和 Linux 平台
|
||||||
|
|
||||||
|
## 故障排查
|
||||||
|
|
||||||
|
### 未找到构建产物
|
||||||
|
|
||||||
|
如果脚本提示"未找到任何构建产物":
|
||||||
|
1. 确保已经运行 `npm run build` 完成构建
|
||||||
|
2. 检查 `src-tauri/target/release/bundle` 目录是否存在
|
||||||
|
3. 确认构建产物文件名包含版本号
|
||||||
|
|
||||||
|
### 签名文件缺失
|
||||||
|
|
||||||
|
如果签名文件缺失:
|
||||||
|
1. 确保设置了 `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` 环境变量
|
||||||
|
2. 确保 `tauri.conf.json` 中 `createUpdaterArtifacts` 设置为 `true`
|
||||||
|
3. 重新构建应用
|
||||||
|
|
||||||
|
### 上传到服务器
|
||||||
|
|
||||||
|
将以下文件上传到服务器:
|
||||||
|
- 安装包文件
|
||||||
|
- 签名文件(`.sig`)
|
||||||
|
- `latest.json` 文件
|
||||||
|
|
||||||
|
确保服务器上的 URL 路径与 `--base-url` 参数匹配。
|
||||||
|
|
||||||
318
scripts/generate-latest-json.js
Normal file
318
scripts/generate-latest-json.js
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 Tauri 更新器的 latest.json 文件
|
||||||
|
*
|
||||||
|
* 使用方法:
|
||||||
|
* node scripts/generate-latest-json.js [options]
|
||||||
|
*
|
||||||
|
* 选项:
|
||||||
|
* --base-url <url> 更新文件的基 URL(必需)
|
||||||
|
* --version <version> 版本号(可选,默认从 tauri.conf.json 读取)
|
||||||
|
* --notes <notes> 更新说明(可选)
|
||||||
|
* --target <target> 构建类型(可选,默认 release,可选值:debug、release、fast-release)
|
||||||
|
* --rename 在生成 latest.json 之前,将文件名中的 "CS工具箱" 改为 "CS_Toolbox"
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// 解析命令行参数
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const getArg = (name, defaultValue = null) => {
|
||||||
|
const index = args.indexOf(name);
|
||||||
|
if (index !== -1 && args[index + 1]) {
|
||||||
|
return args[index + 1];
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 读取配置
|
||||||
|
const tauriConfigPath = path.join(__dirname, '../src-tauri/tauri.conf.json');
|
||||||
|
const tauriConfig = JSON.parse(fs.readFileSync(tauriConfigPath, 'utf-8'));
|
||||||
|
|
||||||
|
const version = getArg('--version', tauriConfig.version);
|
||||||
|
const baseUrl = getArg('--base-url');
|
||||||
|
const notes = getArg('--notes', '');
|
||||||
|
const target = getArg('--target', 'release');
|
||||||
|
const shouldRename = args.includes('--rename');
|
||||||
|
|
||||||
|
// 验证 target 参数
|
||||||
|
const validTargets = ['debug', 'release', 'fast-release'];
|
||||||
|
if (!validTargets.includes(target)) {
|
||||||
|
console.error(`错误: --target 必须是以下值之一: ${validTargets.join(', ')}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!baseUrl) {
|
||||||
|
console.error('错误: 必须提供 --base-url 参数');
|
||||||
|
console.error('示例: node scripts/generate-latest-json.js --base-url https://your-server.com/releases');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果需要重命名,先执行重命名脚本
|
||||||
|
if (shouldRename) {
|
||||||
|
console.log('\n执行文件重命名...');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
try {
|
||||||
|
execSync(`node scripts/rename-build-artifacts.js --target ${target}`, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
cwd: path.join(__dirname, '..')
|
||||||
|
});
|
||||||
|
console.log('✓ 文件重命名完成\n');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('✗ 文件重命名失败:', err.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保 baseUrl 不以 / 结尾
|
||||||
|
const cleanBaseUrl = baseUrl.replace(/\/$/, '');
|
||||||
|
|
||||||
|
// 检测是否是 GitHub releases URL
|
||||||
|
const githubReleasesMatch = cleanBaseUrl.match(/^https?:\/\/github\.com\/([^\/]+)\/([^\/]+)\/releases$/i);
|
||||||
|
let githubOwner = null;
|
||||||
|
let githubRepo = null;
|
||||||
|
if (githubReleasesMatch) {
|
||||||
|
githubOwner = githubReleasesMatch[1];
|
||||||
|
githubRepo = githubReleasesMatch[2];
|
||||||
|
console.log(`检测到 GitHub releases: ${githubOwner}/${githubRepo}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成文件 URL
|
||||||
|
function generateFileUrl(filename) {
|
||||||
|
// 如果使用了 --rename,文件名应该已经是 CS_Toolbox 了
|
||||||
|
// 但为了兼容,我们检查一下是否需要替换
|
||||||
|
let finalFilename = filename;
|
||||||
|
if (shouldRename && filename.includes('CS工具箱')) {
|
||||||
|
finalFilename = filename.replace(/CS工具箱/g, 'CS_Toolbox');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (githubOwner && githubRepo) {
|
||||||
|
// GitHub releases 下载 URL 格式: https://github.com/{owner}/{repo}/releases/download/{tag}/{filename}
|
||||||
|
const tag = version.startsWith('v') ? version : `v${version}`;
|
||||||
|
return `https://github.com/${githubOwner}/${githubRepo}/releases/download/${tag}/${finalFilename}`;
|
||||||
|
}
|
||||||
|
// 普通 URL
|
||||||
|
return `${cleanBaseUrl}/${finalFilename}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据 target 确定构建产物目录
|
||||||
|
const bundleDir = path.join(__dirname, '../src-tauri/target', target, 'bundle');
|
||||||
|
console.log(`使用构建类型: ${target}`);
|
||||||
|
console.log(`构建产物目录: ${bundleDir}`);
|
||||||
|
|
||||||
|
const platforms = {
|
||||||
|
'windows-x86_64': {
|
||||||
|
dir: path.join(bundleDir, 'nsis'),
|
||||||
|
extensions: ['.exe'],
|
||||||
|
pattern: /windows|win/i
|
||||||
|
},
|
||||||
|
'darwin-x86_64': {
|
||||||
|
dir: path.join(bundleDir, 'macos'),
|
||||||
|
extensions: ['.dmg', '.app.tar.gz'],
|
||||||
|
pattern: /darwin|macos|mac/i
|
||||||
|
},
|
||||||
|
'darwin-aarch64': {
|
||||||
|
dir: path.join(bundleDir, 'macos'),
|
||||||
|
extensions: ['.dmg', '.app.tar.gz'],
|
||||||
|
pattern: /darwin|macos|mac|aarch64|arm64/i
|
||||||
|
},
|
||||||
|
'linux-x86_64': {
|
||||||
|
dir: path.join(bundleDir, 'appimage'),
|
||||||
|
extensions: ['.AppImage', '.deb', '.rpm'],
|
||||||
|
pattern: /linux/i
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 递归查找文件
|
||||||
|
function findFilesRecursive(dir, version, extensions, maxDepth = 2, currentDepth = 0) {
|
||||||
|
if (!fs.existsSync(dir) || currentDepth > maxDepth) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const items = fs.readdirSync(dir);
|
||||||
|
const versionPattern = version.replace(/\./g, '\\.').replace(/-/g, '[-.]');
|
||||||
|
const regex = new RegExp(versionPattern, 'i');
|
||||||
|
|
||||||
|
// 先尝试精确匹配版本
|
||||||
|
for (const item of items) {
|
||||||
|
const itemPath = path.join(dir, item);
|
||||||
|
const stat = fs.statSync(itemPath);
|
||||||
|
|
||||||
|
if (stat.isFile()) {
|
||||||
|
if (!regex.test(item)) continue;
|
||||||
|
|
||||||
|
const ext = path.extname(item);
|
||||||
|
if (extensions.includes(ext)) {
|
||||||
|
const sigPath = itemPath + '.sig';
|
||||||
|
|
||||||
|
return {
|
||||||
|
file: item,
|
||||||
|
url: generateFileUrl(item),
|
||||||
|
signature: fs.existsSync(sigPath) ? fs.readFileSync(sigPath, 'utf-8').trim() : null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (stat.isDirectory() && currentDepth < maxDepth) {
|
||||||
|
const result = findFilesRecursive(itemPath, version, extensions, maxDepth, currentDepth + 1);
|
||||||
|
if (result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果精确匹配失败,尝试查找最新版本的文件(仅在同一目录下)
|
||||||
|
if (currentDepth === 0) {
|
||||||
|
const matchingFiles = [];
|
||||||
|
for (const item of items) {
|
||||||
|
const itemPath = path.join(dir, item);
|
||||||
|
const stat = fs.statSync(itemPath);
|
||||||
|
|
||||||
|
if (stat.isFile()) {
|
||||||
|
const ext = path.extname(item);
|
||||||
|
if (extensions.includes(ext)) {
|
||||||
|
matchingFiles.push({
|
||||||
|
file: item,
|
||||||
|
path: itemPath,
|
||||||
|
mtime: stat.mtime
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchingFiles.length > 0) {
|
||||||
|
// 按修改时间排序,选择最新的文件
|
||||||
|
matchingFiles.sort((a, b) => b.mtime - a.mtime);
|
||||||
|
const latest = matchingFiles[0];
|
||||||
|
const sigPath = latest.path + '.sig';
|
||||||
|
|
||||||
|
return {
|
||||||
|
file: latest.file,
|
||||||
|
url: generateFileUrl(latest.file),
|
||||||
|
signature: fs.existsSync(sigPath) ? fs.readFileSync(sigPath, 'utf-8').trim() : null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// 忽略错误,继续查找
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找文件(兼容旧接口)
|
||||||
|
function findFiles(dir, version, extensions) {
|
||||||
|
return findFilesRecursive(dir, version, extensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成 platforms 对象
|
||||||
|
const platformsData = {};
|
||||||
|
for (const [platform, config] of Object.entries(platforms)) {
|
||||||
|
console.log(`\n查找平台 ${platform}:`);
|
||||||
|
console.log(` 目录: ${config.dir}`);
|
||||||
|
console.log(` 扩展名: ${config.extensions.join(', ')}`);
|
||||||
|
|
||||||
|
// 检查目录是否存在
|
||||||
|
if (!fs.existsSync(config.dir)) {
|
||||||
|
console.warn(` 警告: 目录不存在: ${config.dir}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = findFiles(config.dir, version, config.extensions);
|
||||||
|
if (result) {
|
||||||
|
// 如果使用了 --rename,文件名应该已经是 CS_Toolbox 了
|
||||||
|
// 但如果仍然包含 CS工具箱,说明重命名可能失败了,尝试手动处理
|
||||||
|
let fileName = result.file;
|
||||||
|
if (shouldRename && fileName.includes('CS工具箱')) {
|
||||||
|
console.warn(` 警告: 文件 ${fileName} 仍包含中文,尝试查找重命名后的文件...`);
|
||||||
|
const renamedFile = fileName.replace(/CS工具箱/g, 'CS_Toolbox');
|
||||||
|
const renamedPath = path.join(config.dir, renamedFile);
|
||||||
|
if (fs.existsSync(renamedPath)) {
|
||||||
|
fileName = renamedFile;
|
||||||
|
result.file = renamedFile;
|
||||||
|
result.url = generateFileUrl(renamedFile);
|
||||||
|
// 检查重命名后的签名文件
|
||||||
|
const renamedSigPath = renamedPath + '.sig';
|
||||||
|
if (fs.existsSync(renamedSigPath)) {
|
||||||
|
result.signature = fs.readFileSync(renamedSigPath, 'utf-8').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(` ✓ 找到文件: ${fileName}`);
|
||||||
|
platformsData[platform] = {
|
||||||
|
url: result.url,
|
||||||
|
signature: result.signature
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.warn(` ✗ 未找到匹配版本 ${version} 的文件`);
|
||||||
|
// 列出目录中的所有文件以便调试
|
||||||
|
try {
|
||||||
|
const files = fs.readdirSync(config.dir);
|
||||||
|
const exeFiles = files.filter(f => {
|
||||||
|
const ext = path.extname(f);
|
||||||
|
return config.extensions.includes(ext);
|
||||||
|
});
|
||||||
|
if (exeFiles.length > 0) {
|
||||||
|
console.log(` 目录中的文件: ${exeFiles.join(', ')}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// 忽略错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有找到任何平台文件,尝试查找所有文件
|
||||||
|
if (Object.keys(platformsData).length === 0) {
|
||||||
|
console.warn('\n警告: 未找到任何构建产物,将生成空的 platforms 对象');
|
||||||
|
console.warn('请确保已经构建了应用: npm run build');
|
||||||
|
console.warn(`构建产物应该在: ${bundleDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成 latest.json
|
||||||
|
const latestJson = {
|
||||||
|
version: version,
|
||||||
|
notes: notes || `版本 ${version} 更新`,
|
||||||
|
pub_date: new Date().toISOString(),
|
||||||
|
platforms: Object.keys(platformsData).length > 0 ? platformsData : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果只有一个平台,也可以使用简化的格式
|
||||||
|
if (Object.keys(platformsData).length === 1) {
|
||||||
|
const platform = Object.keys(platformsData)[0];
|
||||||
|
latestJson.download_url = platformsData[platform].url;
|
||||||
|
latestJson.signature = platformsData[platform].signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 输出文件到对应的 bundle 目录
|
||||||
|
const outputDir = bundleDir;
|
||||||
|
if (!fs.existsSync(outputDir)) {
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
}
|
||||||
|
// 确保 nsis 子目录存在
|
||||||
|
const nsisOutputDir = path.join(outputDir, "nsis");
|
||||||
|
if (!fs.existsSync(nsisOutputDir)) {
|
||||||
|
fs.mkdirSync(nsisOutputDir, { recursive: true });
|
||||||
|
}
|
||||||
|
const outputPath = path.join(nsisOutputDir, 'latest.json');
|
||||||
|
fs.writeFileSync(outputPath, JSON.stringify(latestJson, null, 2), 'utf-8');
|
||||||
|
|
||||||
|
console.log('✓ 成功生成 latest.json');
|
||||||
|
console.log(` 构建类型: ${target}`);
|
||||||
|
console.log(` 文件位置: ${outputPath}`);
|
||||||
|
console.log(` 版本: ${version}`);
|
||||||
|
console.log(` 平台数量: ${Object.keys(platformsData).length}`);
|
||||||
|
if (Object.keys(platformsData).length > 0) {
|
||||||
|
console.log(` 平台: ${Object.keys(platformsData).join(', ')}`);
|
||||||
|
// 显示签名信息
|
||||||
|
for (const [platform, data] of Object.entries(platformsData)) {
|
||||||
|
if (data.signature) {
|
||||||
|
console.log(` ${platform}: 已找到签名文件`);
|
||||||
|
} else {
|
||||||
|
console.log(` ${platform}: 未找到签名文件`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('\n文件内容:');
|
||||||
|
console.log(JSON.stringify(latestJson, null, 2));
|
||||||
|
|
||||||
142
scripts/rename-build-artifacts.js
Normal file
142
scripts/rename-build-artifacts.js
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量重命名构建产物,将文件名中的 "CS工具箱" 改为 "CS_Toolbox"
|
||||||
|
* 用于解决 GitHub releases 不支持中文文件名的问题
|
||||||
|
*
|
||||||
|
* 使用方法:
|
||||||
|
* node scripts/rename-build-artifacts.js [options]
|
||||||
|
*
|
||||||
|
* 选项:
|
||||||
|
* --target <target> 构建类型(可选,默认 release,可选值:debug、release、fast-release)
|
||||||
|
* --dry-run 仅显示将要重命名的文件,不实际执行
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// 解析命令行参数
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const getArg = (name, defaultValue = null) => {
|
||||||
|
const index = args.indexOf(name);
|
||||||
|
if (index !== -1 && args[index + 1]) {
|
||||||
|
return args[index + 1];
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const target = getArg('--target', 'release');
|
||||||
|
const dryRun = args.includes('--dry-run');
|
||||||
|
|
||||||
|
// 验证 target 参数
|
||||||
|
const validTargets = ['debug', 'release', 'fast-release'];
|
||||||
|
if (!validTargets.includes(target)) {
|
||||||
|
console.error(`错误: --target 必须是以下值之一: ${validTargets.join(', ')}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据 target 确定构建产物目录
|
||||||
|
const bundleDir = path.join(__dirname, '../src-tauri/target', target, 'bundle');
|
||||||
|
console.log(`使用构建类型: ${target}`);
|
||||||
|
console.log(`构建产物目录: ${bundleDir}`);
|
||||||
|
console.log(dryRun ? '(仅预览模式,不会实际重命名)' : '');
|
||||||
|
|
||||||
|
if (!fs.existsSync(bundleDir)) {
|
||||||
|
console.error(`错误: 构建产物目录不存在: ${bundleDir}`);
|
||||||
|
console.error('请先构建应用');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 需要重命名的文件扩展名
|
||||||
|
const extensions = ['.exe', '.dmg', '.AppImage', '.deb', '.rpm', '.app.tar.gz', '.sig'];
|
||||||
|
|
||||||
|
// 递归查找并重命名文件
|
||||||
|
function renameFilesRecursive(dir, maxDepth = 3, currentDepth = 0) {
|
||||||
|
if (!fs.existsSync(dir) || currentDepth > maxDepth) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const renamedFiles = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const items = fs.readdirSync(dir);
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const itemPath = path.join(dir, item);
|
||||||
|
const stat = fs.statSync(itemPath);
|
||||||
|
|
||||||
|
if (stat.isFile()) {
|
||||||
|
// 检查文件名是否包含 "CS工具箱"
|
||||||
|
if (item.includes('CS工具箱')) {
|
||||||
|
const newName = item.replace(/CS工具箱/g, 'CS_Toolbox');
|
||||||
|
const newPath = path.join(dir, newName);
|
||||||
|
|
||||||
|
// 检查新文件名是否已存在
|
||||||
|
if (fs.existsSync(newPath) && itemPath !== newPath) {
|
||||||
|
console.warn(` 警告: 目标文件已存在,跳过: ${newName}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
renamedFiles.push({
|
||||||
|
oldPath: itemPath,
|
||||||
|
newPath: newPath,
|
||||||
|
oldName: item,
|
||||||
|
newName: newName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (stat.isDirectory() && currentDepth < maxDepth) {
|
||||||
|
// 递归查找子目录
|
||||||
|
const subRenamed = renameFilesRecursive(itemPath, maxDepth, currentDepth + 1);
|
||||||
|
renamedFiles.push(...subRenamed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`读取目录时出错 ${dir}:`, err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return renamedFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找所有需要重命名的文件
|
||||||
|
console.log('\n查找需要重命名的文件...');
|
||||||
|
const filesToRename = renameFilesRecursive(bundleDir);
|
||||||
|
|
||||||
|
if (filesToRename.length === 0) {
|
||||||
|
console.log('✓ 未找到需要重命名的文件');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示将要重命名的文件
|
||||||
|
console.log(`\n找到 ${filesToRename.length} 个文件需要重命名:\n`);
|
||||||
|
filesToRename.forEach(({ oldName, newName }) => {
|
||||||
|
console.log(` ${oldName}`);
|
||||||
|
console.log(` → ${newName}\n`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
console.log('(预览模式,未实际执行重命名)');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行重命名
|
||||||
|
console.log('执行重命名...');
|
||||||
|
let successCount = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
|
||||||
|
for (const { oldPath, newPath, oldName, newName } of filesToRename) {
|
||||||
|
try {
|
||||||
|
fs.renameSync(oldPath, newPath);
|
||||||
|
console.log(`✓ ${oldName} → ${newName}`);
|
||||||
|
successCount++;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`✗ 重命名失败 ${oldName}:`, err.message);
|
||||||
|
errorCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n完成: 成功 ${successCount} 个,失败 ${errorCount} 个`);
|
||||||
|
|
||||||
|
if (errorCount > 0) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
2
src-tauri/.gitignore
vendored
2
src-tauri/.gitignore
vendored
@@ -1,3 +1,3 @@
|
|||||||
# Generated by Cargo
|
# Generated by Cargo
|
||||||
# will have compiled files and executables
|
# will have compiled files and executables
|
||||||
/target/
|
/target/
|
||||||
3779
src-tauri/Cargo.lock
generated
3779
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "CS工具箱"
|
name = "CS工具箱"
|
||||||
version = "0.0.1"
|
version = "0.0.6"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["Purp1e"]
|
authors = ["Purp1e"]
|
||||||
license = ""
|
license = ""
|
||||||
@@ -21,37 +21,50 @@ strip = true # Remove debug symbols
|
|||||||
opt-level = 0 # 关闭优化
|
opt-level = 0 # 关闭优化
|
||||||
debug = true # 保留调试信息
|
debug = true # 保留调试信息
|
||||||
|
|
||||||
|
[profile.fast-release]
|
||||||
|
inherits = "release"
|
||||||
|
lto = false # 关闭链接时优化,加快构建速度
|
||||||
|
codegen-units = 16 # 增加并行编译单元,加快构建速度
|
||||||
|
strip = false # 不剥离调试符号,加快构建速度
|
||||||
|
opt-level = 2 # 使用适中的优化级别(比 release 的 "s" 更快)
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.1.0", features = [] }
|
tauri-build = { version = "2.5.1", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = "0.4.26"
|
log = "0.4.28"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
walkdir = "2.5.0"
|
walkdir = "2.5.0"
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.145"
|
||||||
regex = "1.11.1"
|
regex = "1.12.2"
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
reqwest = { version = "0.12.15", features = ["blocking"] }
|
reqwest = { version = "0.12.24", features = ["json", "stream", "blocking"] }
|
||||||
tauri = { version = "2.4.0", features = [ "macos-private-api",
|
futures-util = "0.3.30"
|
||||||
|
tauri = { version = "2.9.2", features = [ "macos-private-api",
|
||||||
"tray-icon"
|
"tray-icon"
|
||||||
] }
|
] }
|
||||||
window-vibrancy = "0.6.0"
|
window-vibrancy = "0.7.1"
|
||||||
tauri-plugin-process = "2.2.0"
|
tauri-plugin-process = "2.3.1"
|
||||||
tauri-plugin-fs = "2.2.0"
|
tauri-plugin-fs = "2.4.4"
|
||||||
tauri-plugin-dialog = "2.2.0"
|
tauri-plugin-dialog = "2.4.2"
|
||||||
tauri-plugin-os = "2.2.1"
|
tauri-plugin-os = "2.3.2"
|
||||||
tauri-plugin-clipboard-manager = "2.2.2"
|
tauri-plugin-clipboard-manager = "2.3.2"
|
||||||
tauri-plugin-shell = "2.2.0"
|
tauri-plugin-shell = "2.3.3"
|
||||||
tauri-plugin-http = "2.4.2"
|
tauri-plugin-http = "2.5.4"
|
||||||
tauri-plugin-notification = "2.2.2"
|
tauri-plugin-notification = "2.3.3"
|
||||||
tauri-plugin-valtio = "2.1.1"
|
tauri-plugin-valtio = "3.2.0"
|
||||||
tauri-plugin-store = "2.2.0"
|
tauri-plugin-store = "2.4.1"
|
||||||
tauri-plugin-system-info = "2.0.9"
|
tauri-plugin-system-info = "2.0.9"
|
||||||
tauri-plugin-theme = "2.1.3"
|
tauri-plugin-autostart = "2.5.1"
|
||||||
tauri-plugin-autostart = "2.2.0"
|
tauri-plugin-single-instance = { version = "2.3.6", features = ["deep-link"] }
|
||||||
tauri-plugin-single-instance = { version = "2.2.2", features = ["deep-link"] }
|
tauri-plugin-deep-link = "2.4.5"
|
||||||
tauri-plugin-deep-link = "2.2.0"
|
anyhow = "1.0.100"
|
||||||
anyhow = "1.0.97"
|
notify = "8.2.0"
|
||||||
|
dirs = "6.0.0"
|
||||||
|
tokio = { version = "1.40", features = ["process"] }
|
||||||
|
gfxinfo = "0.1.2"
|
||||||
|
url = "2.5"
|
||||||
|
md5 = "0.7.0"
|
||||||
[target.'cfg(windows)'.dependencies] # Windows Only
|
[target.'cfg(windows)'.dependencies] # Windows Only
|
||||||
winreg = "0.55.0"
|
winreg = "0.55.0"
|
||||||
|
|
||||||
@@ -64,5 +77,7 @@ default = [ "custom-protocol" ]
|
|||||||
custom-protocol = [ "tauri/custom-protocol" ]
|
custom-protocol = [ "tauri/custom-protocol" ]
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-global-shortcut = "2.2.0"
|
tauri-plugin-cli = "2.4.1"
|
||||||
tauri-plugin-single-instance = "2.2.2"
|
tauri-plugin-global-shortcut = "2.3.1"
|
||||||
|
tauri-plugin-single-instance = "2.3.6"
|
||||||
|
tauri-plugin-updater = "2"
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
],
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"global-shortcut:default",
|
"global-shortcut:default",
|
||||||
"theme:default",
|
|
||||||
"store:default",
|
"store:default",
|
||||||
"store:allow-set",
|
"store:allow-set",
|
||||||
"store:allow-get-store",
|
"store:allow-get-store",
|
||||||
@@ -27,6 +26,12 @@
|
|||||||
"deep-link:allow-get-current",
|
"deep-link:allow-get-current",
|
||||||
"autostart:default",
|
"autostart:default",
|
||||||
"autostart:allow-enable",
|
"autostart:allow-enable",
|
||||||
"autostart:allow-disable"
|
"autostart:allow-disable",
|
||||||
|
"cli:default",
|
||||||
|
"fs:allow-read-text-file",
|
||||||
|
"fs:allow-write-text-file",
|
||||||
|
"fs:allow-resource-read-recursive",
|
||||||
|
"dialog:allow-save",
|
||||||
|
"updater:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
{"desktop-capability":{"identifier":"desktop-capability","description":"","local":true,"windows":["main"],"permissions":["global-shortcut:default","theme:default","store:default","store:allow-set","store:allow-get-store","store:allow-has","store:allow-delete","store:allow-clear","store:allow-values","store:allow-save","store:allow-load","store:allow-reset","store:allow-entries","deep-link:default","deep-link:allow-register","deep-link:allow-get-current","autostart:default","autostart:allow-enable","autostart:allow-disable"],"platforms":["macOS","windows","linux"]},"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["core:default","fs:allow-read-file","fs:allow-write-file","fs:allow-read-dir","fs:allow-copy-file","fs:allow-mkdir","fs:allow-remove","fs:allow-remove","fs:allow-rename","fs:allow-exists","core:window:allow-create","core:window:allow-center","core:window:allow-request-user-attention","core:window:allow-set-resizable","core:window:allow-set-maximizable","core:window:allow-set-minimizable","core:window:allow-set-closable","core:window:allow-set-title","core:window:allow-maximize","core:window:allow-unmaximize","core:window:allow-minimize","core:window:allow-unminimize","core:window:allow-show","core:window:allow-hide","core:window:allow-close","core:window:allow-set-decorations","core:window:allow-set-always-on-top","core:window:allow-set-content-protected","core:window:allow-set-size","core:window:allow-set-min-size","core:window:allow-set-max-size","core:window:allow-set-position","core:window:allow-set-fullscreen","core:window:allow-set-focus","core:window:allow-set-icon","core:window:allow-set-skip-taskbar","core:window:allow-set-cursor-grab","core:window:allow-set-cursor-visible","core:window:allow-set-cursor-icon","core:window:allow-set-cursor-position","core:window:allow-set-ignore-cursor-events","core:window:allow-start-dragging","core:webview:allow-print","shell:allow-execute","shell:allow-open","dialog:allow-open","dialog:allow-save","dialog:allow-message","dialog:allow-ask","dialog:allow-confirm","http:default","notification:default","global-shortcut:allow-is-registered","global-shortcut:allow-register","global-shortcut:allow-register-all","global-shortcut:allow-unregister","global-shortcut:allow-unregister-all","os:allow-platform","os:allow-version","os:allow-os-type","os:allow-family","os:allow-arch","os:allow-exe-extension","os:allow-locale","os:allow-hostname","process:allow-restart","process:allow-exit","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text","core:app:allow-app-show","core:app:allow-app-hide","core:app:allow-set-app-theme","process:default","fs:default","dialog:default","os:default","clipboard-manager:default"]},"system-info":{"identifier":"system-info","description":"","local":true,"windows":["*"],"permissions":["system-info:allow-all"]},"valtio":{"identifier":"valtio","description":"","local":true,"windows":["*"],"permissions":["valtio:default","core:event:default"]}}
|
{"desktop-capability":{"identifier":"desktop-capability","description":"","local":true,"windows":["main"],"permissions":["global-shortcut:default","store:default","store:allow-set","store:allow-get-store","store:allow-has","store:allow-delete","store:allow-clear","store:allow-values","store:allow-save","store:allow-load","store:allow-reset","store:allow-entries","deep-link:default","deep-link:allow-register","deep-link:allow-get-current","autostart:default","autostart:allow-enable","autostart:allow-disable","cli:default","fs:allow-read-text-file","fs:allow-write-text-file","fs:allow-resource-read-recursive","dialog:allow-save","updater:default"],"platforms":["macOS","windows","linux"]},"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["core:default","fs:allow-read-file","fs:allow-write-file","fs:allow-read-dir","fs:allow-copy-file","fs:allow-mkdir","fs:allow-remove","fs:allow-remove","fs:allow-rename","fs:allow-exists","core:window:allow-create","core:window:allow-center","core:window:allow-request-user-attention","core:window:allow-set-resizable","core:window:allow-set-maximizable","core:window:allow-set-minimizable","core:window:allow-set-closable","core:window:allow-set-title","core:window:allow-maximize","core:window:allow-unmaximize","core:window:allow-minimize","core:window:allow-unminimize","core:window:allow-show","core:window:allow-hide","core:window:allow-close","core:window:allow-set-decorations","core:window:allow-set-always-on-top","core:window:allow-set-content-protected","core:window:allow-set-size","core:window:allow-set-min-size","core:window:allow-set-max-size","core:window:allow-set-position","core:window:allow-set-fullscreen","core:window:allow-set-focus","core:window:allow-set-icon","core:window:allow-set-skip-taskbar","core:window:allow-set-cursor-grab","core:window:allow-set-cursor-visible","core:window:allow-set-cursor-icon","core:window:allow-set-cursor-position","core:window:allow-set-ignore-cursor-events","core:window:allow-start-dragging","core:webview:allow-print","shell:allow-execute","shell:allow-open","dialog:allow-open","dialog:allow-save","dialog:allow-message","dialog:allow-ask","dialog:allow-confirm","http:default","notification:default","global-shortcut:allow-is-registered","global-shortcut:allow-register","global-shortcut:allow-register-all","global-shortcut:allow-unregister","global-shortcut:allow-unregister-all","os:allow-platform","os:allow-version","os:allow-os-type","os:allow-family","os:allow-arch","os:allow-exe-extension","os:allow-locale","os:allow-hostname","process:allow-restart","process:allow-exit","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text","core:app:allow-app-show","core:app:allow-app-hide","core:app:allow-set-app-theme","process:default","fs:default","dialog:default","os:default","clipboard-manager:default"]},"system-info":{"identifier":"system-info","description":"","local":true,"windows":["*"],"permissions":["system-info:allow-all"]},"valtio":{"identifier":"valtio","description":"","local":true,"windows":["*"],"permissions":["valtio:default","core:event:default"]}}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
BIN
src-tauri/resources/csda.exe
Normal file
BIN
src-tauri/resources/csda.exe
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -4,14 +4,18 @@
|
|||||||
)]
|
)]
|
||||||
|
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
|
use tauri_plugin_store::StoreExt;
|
||||||
use tauri_plugin_deep_link::DeepLinkExt;
|
use tauri_plugin_deep_link::DeepLinkExt;
|
||||||
|
use tauri_plugin_cli::CliExt;
|
||||||
use tauri_plugin_autostart::MacosLauncher;
|
use tauri_plugin_autostart::MacosLauncher;
|
||||||
|
|
||||||
// Window Vibrancy
|
// Window Vibrancy
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
use window_vibrancy::apply_mica;
|
use window_vibrancy::apply_acrylic;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial};
|
use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
@@ -36,21 +40,28 @@ fn on_button_clicked() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut ctx = tauri::generate_context!();
|
// 获取应用上下文
|
||||||
|
let ctx = tauri::generate_context!();
|
||||||
|
|
||||||
tauri::Builder::default()
|
// 手动构建 AppConfig 目录路径
|
||||||
.plugin(tauri_plugin_single_instance::init(|app, _, _| {
|
let config_dir = dirs::config_dir().expect("无法获取配置目录");
|
||||||
let _ = app
|
let app_name = ctx.config().identifier.as_str();
|
||||||
.get_webview_window("main")
|
let store_dir = config_dir.join(app_name).join("cstb");
|
||||||
.expect("no main window")
|
|
||||||
.set_focus();
|
let mut builder = tauri::Builder::default()
|
||||||
}))
|
.plugin(tauri_plugin_single_instance::init(|app, _, _| {
|
||||||
|
let window = app.get_webview_window("main").expect("no main window");
|
||||||
|
|
||||||
|
window.show().expect("no main window, can't show");
|
||||||
|
window.set_focus().expect("no main window, can't set focus")
|
||||||
|
}))
|
||||||
.plugin(tauri_plugin_deep_link::init())
|
.plugin(tauri_plugin_deep_link::init())
|
||||||
.plugin(tauri_plugin_valtio::init())
|
|
||||||
.plugin(tauri_plugin_store::Builder::new().build())
|
.plugin(tauri_plugin_store::Builder::new().build())
|
||||||
.plugin(tauri_plugin_theme::init(ctx.config_mut()))
|
|
||||||
.plugin(tauri_plugin_notification::init())
|
.plugin(tauri_plugin_notification::init())
|
||||||
.plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, Some(vec![]) /* arbitrary number of args to pass to your app */))
|
.plugin(tauri_plugin_autostart::init(
|
||||||
|
MacosLauncher::LaunchAgent,
|
||||||
|
Some(vec!["hidden"]), /* arbitrary number of args to pass to your app */
|
||||||
|
))
|
||||||
.plugin(tauri_plugin_http::init())
|
.plugin(tauri_plugin_http::init())
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||||
@@ -60,9 +71,51 @@ fn main() {
|
|||||||
.plugin(tauri_plugin_process::init())
|
.plugin(tauri_plugin_process::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_system_info::init())
|
.plugin(tauri_plugin_system_info::init())
|
||||||
// .plugin(tauri_plugin_store::Builder::default().build())
|
.plugin(tauri_plugin_cli::init())
|
||||||
// .plugin(tauri_plugin_updater::Builder::new().build())
|
// .plugin(tauri_plugin_valtio::init())
|
||||||
.setup(|app| {
|
.plugin(tauri_plugin_valtio::Builder::new().path(&store_dir).build());
|
||||||
|
|
||||||
|
#[cfg(desktop)]
|
||||||
|
{
|
||||||
|
builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.setup(move |app| {
|
||||||
|
// Get Window
|
||||||
|
let window = app.get_webview_window("main").unwrap();
|
||||||
|
|
||||||
|
let store = app.store("cstb.json")?;
|
||||||
|
// 获取boolean类型的hidden值,Err时设置为False
|
||||||
|
let hidden: bool = store
|
||||||
|
.get("hidden")
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
// Vibrant Window - 使用更优雅的错误处理和延迟应用
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
if let Err(e) =
|
||||||
|
apply_vibrancy(&window, NSVisualEffectMaterial::HudWindow, None, Some(10.0))
|
||||||
|
{
|
||||||
|
eprintln!("Failed to apply vibrancy effect: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
// 延迟应用 acrylic 效果,确保窗口完全初始化
|
||||||
|
let window_handle = window.clone();
|
||||||
|
thread::spawn(move || {
|
||||||
|
thread::sleep(Duration::from_millis(100));
|
||||||
|
if let Err(e) = apply_acrylic(&window_handle, None) {
|
||||||
|
eprintln!("Failed to apply acrylic effect: {:?}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply_blur(&window, Some((18, 18, 18, 0)))
|
||||||
|
// .expect("Unsupported platform! 'apply_blur' is only supported on Windows");
|
||||||
|
|
||||||
// Deep Link
|
// Deep Link
|
||||||
#[cfg(desktop)]
|
#[cfg(desktop)]
|
||||||
app.deep_link().register("cstb")?;
|
app.deep_link().register("cstb")?;
|
||||||
@@ -74,35 +127,59 @@ fn main() {
|
|||||||
tray::create_tray(handle)?;
|
tray::create_tray(handle)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Window
|
// CLI
|
||||||
let window = app.get_webview_window("main").unwrap();
|
match app.cli().matches() {
|
||||||
|
// `matches` here is a Struct with { args, subcommand }.
|
||||||
|
// `args` is `HashMap<String, ArgData>` where `ArgData` is a struct with { value, occurrences }.
|
||||||
|
// `subcommand` is `Option<Box<SubcommandMatches>>` where `SubcommandMatches` is a struct with { name, matches }.
|
||||||
|
Ok(matches) => {
|
||||||
|
println!("{:?}", matches);
|
||||||
|
if matches.args.contains_key("hidden")
|
||||||
|
&& matches.args["hidden"].value == true
|
||||||
|
&& hidden
|
||||||
|
{
|
||||||
|
window.hide().unwrap();
|
||||||
|
} else {
|
||||||
|
window.show().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
|
||||||
// Vibrant Window
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
apply_vibrancy(&window, NSVisualEffectMaterial::HudWindow, None, Some(10.0))
|
|
||||||
.expect("Unsupported platform! 'apply_vibrancy' is only supported on macOS");
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
apply_mica(&window, Some(false))
|
|
||||||
.expect("Unsupported platform! 'apply_mica' is only supported on Windows");
|
|
||||||
|
|
||||||
// apply_blur(&window, Some((18, 18, 18, 0)))
|
|
||||||
// .expect("Unsupported platform! 'apply_blur' is only supported on Windows");
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
cmds::greet,
|
cmds::greet,
|
||||||
cmds::launch_game,
|
cmds::launch_game,
|
||||||
cmds::kill_game,
|
cmds::kill_game,
|
||||||
|
cmds::check_process_running,
|
||||||
cmds::kill_steam,
|
cmds::kill_steam,
|
||||||
cmds::get_steam_path,
|
cmds::get_steam_path,
|
||||||
cmds::get_cs_path,
|
cmds::get_cs_path,
|
||||||
cmds::open_path,
|
cmds::open_path,
|
||||||
cmds::get_powerplan,
|
cmds::get_powerplan,
|
||||||
cmds::set_powerplan,
|
cmds::set_powerplan,
|
||||||
cmds::get_steam_users,
|
cmds::get_steam_users,
|
||||||
cmds::set_auto_login_user,
|
cmds::set_auto_login_user,
|
||||||
|
cmds::start_watch_loginusers,
|
||||||
|
cmds::start_watch_cs2_video,
|
||||||
|
cmds::stop_watch_cs2_video,
|
||||||
|
cmds::get_cs2_video_config,
|
||||||
|
cmds::set_cs2_video_config,
|
||||||
cmds::check_path,
|
cmds::check_path,
|
||||||
|
cmds::check_steam_dir_valid,
|
||||||
|
cmds::analyze_replay,
|
||||||
|
cmds::get_console_log_path,
|
||||||
|
cmds::read_vprof_report,
|
||||||
|
cmds::check_app_update,
|
||||||
|
cmds::download_app_update,
|
||||||
|
cmds::cancel_download_update,
|
||||||
|
cmds::install_app_update,
|
||||||
|
cmds::get_computer_info,
|
||||||
|
cmds::get_gpu_info,
|
||||||
|
cmds::get_memory_info,
|
||||||
|
cmds::get_monitor_info,
|
||||||
|
cmds::get_motherboard_info,
|
||||||
on_button_clicked
|
on_button_clicked
|
||||||
])
|
])
|
||||||
.run(ctx)
|
.run(ctx)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ pub mod id;
|
|||||||
pub mod path;
|
pub mod path;
|
||||||
pub mod reg;
|
pub mod reg;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
pub mod watch;
|
||||||
|
|
||||||
// common steam utils
|
// common steam utils
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -24,11 +25,11 @@ pub fn launch_game(
|
|||||||
|
|
||||||
let mut opt = launch_option.replace("\n", " ");
|
let mut opt = launch_option.replace("\n", " ");
|
||||||
|
|
||||||
if server == "perfectworld" {
|
opt = match server {
|
||||||
opt = opt.replace("-worldwide", "") + " -perfectworld";
|
"perfectworld" => opt.replace("-worldwide", "") + " -perfectworld",
|
||||||
} else if server == "worldwide" {
|
"worldwide" => opt.replace("-perfectworld", "") + " -worldwide",
|
||||||
opt = opt.replace("-perfectworld", "") + " -worldwide";
|
_ => opt,
|
||||||
}
|
};
|
||||||
|
|
||||||
let opts = format!("-applaunch 730 {}", opt);
|
let opts = format!("-applaunch 730 {}", opt);
|
||||||
let opts_split = opts.split_whitespace().collect::<Vec<&str>>();
|
let opts_split = opts.split_whitespace().collect::<Vec<&str>>();
|
||||||
|
|||||||
@@ -14,11 +14,9 @@ pub fn get_steam_users() -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_steam_users() {
|
fn test_get_steam_users() {
|
||||||
let result = get_steam_users();
|
let result = super::get_steam_users();
|
||||||
assert!(result.is_ok() || result.is_err());
|
assert!(result.is_ok() || result.is_err());
|
||||||
println!("{}", result.unwrap());
|
println!("{}", result.unwrap());
|
||||||
}
|
}
|
||||||
|
|||||||
148
src-tauri/src/steam/watch.rs
Normal file
148
src-tauri/src/steam/watch.rs
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
use tauri::{AppHandle, Emitter};
|
||||||
|
|
||||||
|
// 全局 watcher 存储,用于管理文件监听器的生命周期
|
||||||
|
static WATCHER: OnceLock<Mutex<Option<RecommendedWatcher>>> = OnceLock::new();
|
||||||
|
static CS2_VIDEO_WATCHER: OnceLock<Mutex<Option<RecommendedWatcher>>> = OnceLock::new();
|
||||||
|
|
||||||
|
/// 启动监听 loginusers.vdf 文件的变化
|
||||||
|
pub fn start_watch_loginusers(app: AppHandle, steam_dir: String) -> Result<()> {
|
||||||
|
// 停止之前的监听
|
||||||
|
stop_watch_loginusers();
|
||||||
|
|
||||||
|
let loginusers_path = Path::new(&steam_dir).join("config/loginusers.vdf");
|
||||||
|
let config_dir = Path::new(&steam_dir).join("config");
|
||||||
|
|
||||||
|
// 如果 config 目录不存在,不进行监听
|
||||||
|
if !config_dir.exists() {
|
||||||
|
log::warn!("config 目录不存在,跳过监听: {:?}", config_dir);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let app_clone = app.clone();
|
||||||
|
|
||||||
|
let mut watcher = RecommendedWatcher::new(
|
||||||
|
move |result: Result<Event, notify::Error>| {
|
||||||
|
match result {
|
||||||
|
Ok(event) => {
|
||||||
|
// 检查是否是 loginusers.vdf 文件的变化
|
||||||
|
if let EventKind::Modify(_) | EventKind::Create(_) = event.kind {
|
||||||
|
for path in &event.paths {
|
||||||
|
if path.ends_with("loginusers.vdf") {
|
||||||
|
log::info!("检测到 loginusers.vdf 文件变化: {:?}", path);
|
||||||
|
// 延迟一小段时间,确保文件写入完成
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
|
// 发送事件到前端
|
||||||
|
if let Err(e) = app_clone.emit("steam://loginusers_changed", ()) {
|
||||||
|
log::error!("发送 loginusers 变化事件失败: {}", e);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("文件监听错误: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Config::default(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// 监听 config 目录(而不是单个文件),因为某些文件系统可能不会直接监听单个文件
|
||||||
|
watcher.watch(&config_dir, RecursiveMode::NonRecursive)?;
|
||||||
|
log::info!("开始监听 loginusers.vdf 文件: {:?}", loginusers_path);
|
||||||
|
|
||||||
|
// 保存 watcher 到全局变量
|
||||||
|
let watcher_store = WATCHER.get_or_init(|| Mutex::new(None));
|
||||||
|
*watcher_store.lock().unwrap() = Some(watcher);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 停止监听 loginusers.vdf 文件
|
||||||
|
pub fn stop_watch_loginusers() {
|
||||||
|
if let Some(watcher_store) = WATCHER.get() {
|
||||||
|
if let Ok(mut watcher_guard) = watcher_store.lock() {
|
||||||
|
if let Some(watcher) = watcher_guard.take() {
|
||||||
|
drop(watcher);
|
||||||
|
log::info!("已停止监听 loginusers.vdf 文件");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 启动监听 cs2_video.txt 文件的变化
|
||||||
|
pub fn start_watch_cs2_video(app: AppHandle, steam_dir: String, steam_id32: u32) -> Result<()> {
|
||||||
|
// 停止之前的监听
|
||||||
|
stop_watch_cs2_video();
|
||||||
|
|
||||||
|
let cfg_dir = Path::new(&steam_dir)
|
||||||
|
.join("userdata")
|
||||||
|
.join(steam_id32.to_string())
|
||||||
|
.join("730")
|
||||||
|
.join("local")
|
||||||
|
.join("cfg");
|
||||||
|
|
||||||
|
// 如果 cfg 目录不存在,不进行监听
|
||||||
|
if !cfg_dir.exists() {
|
||||||
|
log::warn!("cs2_video.txt 配置目录不存在,跳过监听: {:?}", cfg_dir);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let app_clone = app.clone();
|
||||||
|
|
||||||
|
let mut watcher = RecommendedWatcher::new(
|
||||||
|
move |result: Result<Event, notify::Error>| {
|
||||||
|
match result {
|
||||||
|
Ok(event) => {
|
||||||
|
// 检查是否是 cs2_video.txt 文件的变化
|
||||||
|
if let EventKind::Modify(_) | EventKind::Create(_) = event.kind {
|
||||||
|
for path in &event.paths {
|
||||||
|
if path.ends_with("cs2_video.txt") {
|
||||||
|
log::info!("检测到 cs2_video.txt 文件变化: {:?}", path);
|
||||||
|
// 延迟一小段时间,确保文件写入完成
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||||
|
// 发送事件到前端
|
||||||
|
if let Err(e) = app_clone.emit("steam://cs2_video_changed", ()) {
|
||||||
|
log::error!("发送 cs2_video 变化事件失败: {}", e);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("文件监听错误: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Config::default(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// 监听 cfg 目录(而不是单个文件),因为某些文件系统可能不会直接监听单个文件
|
||||||
|
watcher.watch(&cfg_dir, RecursiveMode::NonRecursive)?;
|
||||||
|
log::info!("开始监听 cs2_video.txt 文件: {:?}", cfg_dir.join("cs2_video.txt"));
|
||||||
|
|
||||||
|
// 保存 watcher 到全局变量
|
||||||
|
let watcher_store = CS2_VIDEO_WATCHER.get_or_init(|| Mutex::new(None));
|
||||||
|
*watcher_store.lock().unwrap() = Some(watcher);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 停止监听 cs2_video.txt 文件
|
||||||
|
pub fn stop_watch_cs2_video() {
|
||||||
|
if let Some(watcher_store) = CS2_VIDEO_WATCHER.get() {
|
||||||
|
if let Ok(mut watcher_guard) = watcher_store.lock() {
|
||||||
|
if let Some(watcher) = watcher_guard.take() {
|
||||||
|
drop(watcher);
|
||||||
|
log::info!("已停止监听 cs2_video.txt 文件");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
use std::os::windows::process::CommandExt;
|
use std::fs;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||||
// const DETACHED_PROCESS: u32 = 0x00000008;
|
// const DETACHED_PROCESS: u32 = 0x00000008;
|
||||||
|
|
||||||
pub fn kill(name: &str) -> String {
|
pub fn kill(name: &str) -> String {
|
||||||
|
#[cfg(windows)]
|
||||||
Command::new("taskkill")
|
Command::new("taskkill")
|
||||||
.args(&["/IM", name, "/F"])
|
.args(&["/IM", name, "/F"])
|
||||||
.creation_flags(CREATE_NO_WINDOW)
|
.creation_flags(CREATE_NO_WINDOW)
|
||||||
@@ -15,12 +18,15 @@ pub fn kill(name: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_steam() -> std::io::Result<std::process::Output> {
|
pub fn run_steam() -> std::io::Result<std::process::Output> {
|
||||||
Command::new("cmd")
|
#[cfg(target_os = "windows")]
|
||||||
|
return Command::new("cmd")
|
||||||
.args(&["/C", "start", "steam://run"])
|
.args(&["/C", "start", "steam://run"])
|
||||||
.creation_flags(CREATE_NO_WINDOW)
|
.creation_flags(CREATE_NO_WINDOW)
|
||||||
.output()
|
.output();
|
||||||
}
|
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
Command::new("open").args(&["-a", "Steam"]).output()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_exe_path(name: &str) -> Result<String, std::io::Error> {
|
pub fn get_exe_path(name: &str) -> Result<String, std::io::Error> {
|
||||||
// [原理]
|
// [原理]
|
||||||
@@ -30,12 +36,26 @@ pub fn get_exe_path(name: &str) -> Result<String, std::io::Error> {
|
|||||||
// ----
|
// ----
|
||||||
// 进程路径
|
// 进程路径
|
||||||
let command = format!("Get-Process {} | Select-Object path", name);
|
let command = format!("Get-Process {} | Select-Object path", name);
|
||||||
let args = command.split_whitespace().collect::<Vec<&str>>();
|
#[cfg(windows)]
|
||||||
let output = Command::new("powershell.exe")
|
let output = Command::new("powershell.exe")
|
||||||
.args(&args)
|
.args(&[
|
||||||
|
"-NoProfile",
|
||||||
|
"-WindowStyle",
|
||||||
|
"Hidden",
|
||||||
|
"-Command",
|
||||||
|
&command,
|
||||||
|
])
|
||||||
.creation_flags(CREATE_NO_WINDOW)
|
.creation_flags(CREATE_NO_WINDOW)
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let output = Command::new("osascript")
|
||||||
|
.args(&[
|
||||||
|
"-e",
|
||||||
|
&format!("tell application \"{}\" to get path to me", name),
|
||||||
|
])
|
||||||
|
.output()?;
|
||||||
|
|
||||||
let out = String::from_utf8_lossy(&output.stdout).to_string();
|
let out = String::from_utf8_lossy(&output.stdout).to_string();
|
||||||
|
|
||||||
if out.contains("Path") {
|
if out.contains("Path") {
|
||||||
@@ -55,6 +75,9 @@ pub fn get_exe_path(name: &str) -> Result<String, std::io::Error> {
|
|||||||
pub fn open_path(path: &str) -> Result<(), std::io::Error> {
|
pub fn open_path(path: &str) -> Result<(), std::io::Error> {
|
||||||
// path中所有/ 转换为 \
|
// path中所有/ 转换为 \
|
||||||
let path = path.replace("/", "\\");
|
let path = path.replace("/", "\\");
|
||||||
|
fs::create_dir_all(&path)?;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
Command::new("cmd.exe")
|
Command::new("cmd.exe")
|
||||||
.args(["/c", "start", "", &path])
|
.args(["/c", "start", "", &path])
|
||||||
.creation_flags(CREATE_NO_WINDOW)
|
.creation_flags(CREATE_NO_WINDOW)
|
||||||
@@ -63,28 +86,96 @@ pub fn open_path(path: &str) -> Result<(), std::io::Error> {
|
|||||||
Ok(())
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异步版本的进程检测函数
|
||||||
|
pub async fn check_process_running_async(name: &str) -> bool {
|
||||||
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
// 使用tasklist命令检查进程是否存在
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let mut cmd = Command::new("tasklist");
|
||||||
|
cmd.args(&["/FI", &format!("IMAGENAME eq {}", name)]);
|
||||||
|
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||||
|
|
||||||
|
match cmd.output().await {
|
||||||
|
Ok(output) => {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
// 检查输出中是否包含进程名(排除表头)
|
||||||
|
stdout.contains(name) && stdout.contains("exe")
|
||||||
|
}
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
// 对于非Windows系统,可以使用pgrep命令
|
||||||
|
let mut cmd = Command::new("pgrep");
|
||||||
|
cmd.arg("-f");
|
||||||
|
cmd.arg(name);
|
||||||
|
|
||||||
|
match cmd.output().await {
|
||||||
|
Ok(output) => !output.stdout.is_empty(),
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_open_path() {
|
fn test_open_path() {
|
||||||
let path = "D:\\Programs\\Steam";
|
let path = "D:\\Programs\\Steam";
|
||||||
println!("test open path: {}", path);
|
println!("test open path: {}", path);
|
||||||
open_path(path).unwrap();
|
super::open_path(path).unwrap();
|
||||||
|
|
||||||
let path = "D:\\Programs\\Steam\\steamapps\\common\\Counter-Strike Global Offensive\\game\\bin\\win64";
|
let path = "D:\\Programs\\Steam\\steamapps\\common\\Counter-Strike Global Offensive\\game\\bin\\win64";
|
||||||
println!("test open path: {}", path);
|
println!("test open path: {}", path);
|
||||||
open_path(path).unwrap();
|
super::open_path(path).unwrap();
|
||||||
|
|
||||||
let path = "%appdata%/Wmpvp/demo";
|
let path = "%appdata%/Wmpvp/demo";
|
||||||
println!("test open path: {}", path);
|
println!("test open path: {}", path);
|
||||||
open_path(path).unwrap()
|
super::open_path(path).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_exe_path() {
|
fn test_get_exe_path() {
|
||||||
let path = get_exe_path("steam").expect("failed");
|
let path = super::get_exe_path("steam").expect("failed");
|
||||||
println!("test get steam path: {}", path);
|
println!("test get steam path: {}", path);
|
||||||
|
|
||||||
get_exe_path("not_running").expect("failed");
|
super::get_exe_path("not_running").expect("failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,4 +42,4 @@ macro_rules! wrap_err {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod common;
|
pub mod common;
|
||||||
pub mod macros;
|
pub mod macros;
|
||||||
pub mod powerplan;
|
pub mod powerplan;
|
||||||
|
// pub mod updater; // 已迁移到官方 tauri-plugin-updater
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ impl PowerPlan {
|
|||||||
.get(&mode)
|
.get(&mode)
|
||||||
.ok_or("Invalid power plan number (expect from 1 to 4)")?;
|
.ok_or("Invalid power plan number (expect from 1 to 4)")?;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
let output = Command::new("powercfg")
|
let output = Command::new("powercfg")
|
||||||
.arg("/S")
|
.arg("/S")
|
||||||
.arg(guid)
|
.arg(guid)
|
||||||
@@ -46,6 +47,14 @@ impl PowerPlan {
|
|||||||
.output()
|
.output()
|
||||||
.map_err(|e| format!("Failed to execute powercfg command: {}", e))?;
|
.map_err(|e| format!("Failed to execute powercfg command: {}", e))?;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let output = Command::new("pmset")
|
||||||
|
.arg("-g")
|
||||||
|
.arg("plan")
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("Failed to execute pmset command: {}", e))?;
|
||||||
|
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Powercfg command failed: {}",
|
"Powercfg command failed: {}",
|
||||||
@@ -57,12 +66,20 @@ impl PowerPlan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self) -> Result<i32, String> {
|
pub fn get(&self) -> Result<i32, String> {
|
||||||
|
#[cfg(windows)]
|
||||||
let output = Command::new("powercfg")
|
let output = Command::new("powercfg")
|
||||||
.arg("/L")
|
.arg("/L")
|
||||||
.creation_flags(CREATE_NO_WINDOW)
|
.creation_flags(CREATE_NO_WINDOW)
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| format!("Failed to execute powercfg command: {}", e))?;
|
.map_err(|e| format!("Failed to execute powercfg command: {}", e))?;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let output = Command::new("pmset")
|
||||||
|
.arg("-g")
|
||||||
|
.arg("plan")
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("Failed to execute pmset command: {}", e))?;
|
||||||
|
|
||||||
let output_str = String::from_utf8_lossy(&output.stdout);
|
let output_str = String::from_utf8_lossy(&output.stdout);
|
||||||
let re = regex::Regex::new(r"GUID:\s+(\S+)\s+\(\S+\)\s+\*")
|
let re = regex::Regex::new(r"GUID:\s+(\S+)\s+\(\S+\)\s+\*")
|
||||||
.map_err(|e| format!("Failed to compile regex: {}", e))?;
|
.map_err(|e| format!("Failed to compile regex: {}", e))?;
|
||||||
|
|||||||
@@ -1,12 +1,175 @@
|
|||||||
use tauri::{
|
use tauri::{
|
||||||
menu::{Menu, MenuItem},
|
menu::{CheckMenuItem, Menu, MenuItem, PredefinedMenuItem, Submenu},
|
||||||
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||||||
Manager, Runtime,
|
Emitter, Listener, Manager, Runtime,
|
||||||
};
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::tool::powerplan::PowerPlanMode;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct LaunchOption {
|
||||||
|
option: String,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
||||||
let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
|
// 托盘菜单项目
|
||||||
let menu = Menu::with_items(app, &[&quit_i])?;
|
let separator = &PredefinedMenuItem::separator(app).unwrap();
|
||||||
|
|
||||||
|
let show_i = &MenuItem::with_id(app, "show", "显示主界面", true, None::<&str>)?;
|
||||||
|
let quit_i = &MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
|
||||||
|
|
||||||
|
let kill_game_i = &MenuItem::with_id(app, "kill_game", "关闭CS2", true, None::<&str>)?;
|
||||||
|
let kill_steam_i = &MenuItem::with_id(app, "kill_steam", "关闭Steam", true, None::<&str>)?;
|
||||||
|
|
||||||
|
let launch_ww_i = &MenuItem::with_id(app, "launch_ww", "启动国际服", true, None::<&str>)?;
|
||||||
|
let launch_pw_i = &MenuItem::with_id(app, "launch_pw", "启动国服", true, None::<&str>)?;
|
||||||
|
|
||||||
|
let power_plan_extreme = CheckMenuItem::with_id(
|
||||||
|
app,
|
||||||
|
"power_plan_extreme",
|
||||||
|
"卓越性能",
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
None::<&str>,
|
||||||
|
)?;
|
||||||
|
let power_plan_high =
|
||||||
|
CheckMenuItem::with_id(app, "power_plan_high", "高性能", true, false, None::<&str>)?;
|
||||||
|
let power_plan_balanced = CheckMenuItem::with_id(
|
||||||
|
app,
|
||||||
|
"power_plan_balanced",
|
||||||
|
"平衡",
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
None::<&str>,
|
||||||
|
)?;
|
||||||
|
let power_plan_powersave = CheckMenuItem::with_id(
|
||||||
|
app,
|
||||||
|
"power_plan_powersave",
|
||||||
|
"节能",
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
None::<&str>,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
|
||||||
|
// 创建启动项子菜单(初始为空,后续会动态更新)
|
||||||
|
let launch_option_submenu = Submenu::with_id(app, "launch_option_submenu", "启动项: 游戏", true)?;
|
||||||
|
|
||||||
|
// 创建托盘菜单
|
||||||
|
let menu = Menu::with_items(
|
||||||
|
app,
|
||||||
|
&[
|
||||||
|
&power_plan_extreme,
|
||||||
|
&power_plan_high,
|
||||||
|
&power_plan_balanced,
|
||||||
|
&power_plan_powersave,
|
||||||
|
separator,
|
||||||
|
&launch_option_submenu,
|
||||||
|
launch_ww_i,
|
||||||
|
launch_pw_i,
|
||||||
|
separator,
|
||||||
|
kill_game_i,
|
||||||
|
kill_steam_i,
|
||||||
|
separator,
|
||||||
|
show_i,
|
||||||
|
quit_i,
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let _ = app.listen("tray://get_powerplan", move |event| {
|
||||||
|
if let Ok(payload) = event.payload().parse::<i32>() {
|
||||||
|
match payload {
|
||||||
|
x if x == PowerPlanMode::Other as i32 => {
|
||||||
|
let _ = power_plan_powersave.set_checked(false);
|
||||||
|
let _ = power_plan_balanced.set_checked(false);
|
||||||
|
let _ = power_plan_high.set_checked(false);
|
||||||
|
let _ = power_plan_extreme.set_checked(false);
|
||||||
|
}
|
||||||
|
x if x == PowerPlanMode::PowerSaving as i32 => {
|
||||||
|
let _ = power_plan_powersave.set_checked(true);
|
||||||
|
let _ = power_plan_balanced.set_checked(false);
|
||||||
|
let _ = power_plan_high.set_checked(false);
|
||||||
|
let _ = power_plan_extreme.set_checked(false);
|
||||||
|
}
|
||||||
|
x if x == PowerPlanMode::Balanced as i32 => {
|
||||||
|
let _ = power_plan_powersave.set_checked(false);
|
||||||
|
let _ = power_plan_balanced.set_checked(true);
|
||||||
|
let _ = power_plan_high.set_checked(false);
|
||||||
|
let _ = power_plan_extreme.set_checked(false);
|
||||||
|
}
|
||||||
|
x if x == PowerPlanMode::HighPerformance as i32 => {
|
||||||
|
let _ = power_plan_powersave.set_checked(false);
|
||||||
|
let _ = power_plan_balanced.set_checked(false);
|
||||||
|
let _ = power_plan_high.set_checked(true);
|
||||||
|
let _ = power_plan_extreme.set_checked(false);
|
||||||
|
}
|
||||||
|
x if x == PowerPlanMode::Extreme as i32 => {
|
||||||
|
let _ = power_plan_powersave.set_checked(false);
|
||||||
|
let _ = power_plan_balanced.set_checked(false);
|
||||||
|
let _ = power_plan_high.set_checked(false);
|
||||||
|
let _ = power_plan_extreme.set_checked(true);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听启动项列表更新事件
|
||||||
|
let launch_option_submenu_clone = launch_option_submenu.clone();
|
||||||
|
let _ = app.listen("tray://update_launch_options", move |event| {
|
||||||
|
if let Ok(data) = serde_json::from_str::<serde_json::Value>(event.payload()) {
|
||||||
|
if let (Some(options), Some(current_index)) = (
|
||||||
|
data.get("options").and_then(|v| v.as_array()),
|
||||||
|
data.get("currentIndex").and_then(|v| v.as_u64()),
|
||||||
|
) {
|
||||||
|
let current_index = current_index as usize;
|
||||||
|
// 获取当前启动项名称
|
||||||
|
let current_name = options
|
||||||
|
.get(current_index)
|
||||||
|
.and_then(|opt| opt.get("name"))
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.unwrap_or_else(|| format!("{}", current_index + 1));
|
||||||
|
|
||||||
|
// 更新子菜单标题
|
||||||
|
let _ = launch_option_submenu_clone.set_text(format!("启动项: {}", current_name));
|
||||||
|
|
||||||
|
// 清空现有子菜单项 - 先收集所有项目,然后移除
|
||||||
|
if let Ok(items) = launch_option_submenu_clone.items() {
|
||||||
|
let items_to_remove: Vec<_> = items.iter().collect();
|
||||||
|
for item in items_to_remove {
|
||||||
|
let _ = launch_option_submenu_clone.remove(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新的子菜单项
|
||||||
|
for (index, option) in options.iter().enumerate() {
|
||||||
|
if let Some(name) = option.get("name").and_then(|n| n.as_str()) {
|
||||||
|
let display_name = if name.is_empty() {
|
||||||
|
format!("{}", index + 1)
|
||||||
|
} else {
|
||||||
|
name.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let item_id = format!("launch_option_{}", index);
|
||||||
|
let app_handle = launch_option_submenu_clone.app_handle();
|
||||||
|
if let Ok(item) = CheckMenuItem::with_id(
|
||||||
|
app_handle,
|
||||||
|
&item_id,
|
||||||
|
&display_name,
|
||||||
|
true,
|
||||||
|
index == current_index,
|
||||||
|
None::<&str>,
|
||||||
|
) {
|
||||||
|
let _ = launch_option_submenu_clone.append(&item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let _ = TrayIconBuilder::with_id("tray")
|
let _ = TrayIconBuilder::with_id("tray")
|
||||||
.icon(app.default_window_icon().unwrap().clone())
|
.icon(app.default_window_icon().unwrap().clone())
|
||||||
@@ -16,7 +179,49 @@ pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
|||||||
"quit" => {
|
"quit" => {
|
||||||
app.exit(0);
|
app.exit(0);
|
||||||
}
|
}
|
||||||
// Add more events here
|
"show" => {
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let _ = window.show();
|
||||||
|
let _ = window.set_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"launch_ww" => {
|
||||||
|
let _ = app.emit("tray://launch_game", "worldwide");
|
||||||
|
}
|
||||||
|
"launch_pw" => {
|
||||||
|
let _ = app.emit("tray://launch_game", "perfectworld");
|
||||||
|
}
|
||||||
|
"kill_game" => {
|
||||||
|
let _ = app.emit("tray://kill_game", None::<()>);
|
||||||
|
}
|
||||||
|
"kill_steam" => {
|
||||||
|
let _ = app.emit("tray://kill_steam", None::<()>);
|
||||||
|
}
|
||||||
|
"power_plan_extreme" => {
|
||||||
|
let _ = app.emit("tray://set_powerplan", PowerPlanMode::Extreme as i32);
|
||||||
|
// let _ = power_plan_extreme.set_checked(true);
|
||||||
|
}
|
||||||
|
"power_plan_high" => {
|
||||||
|
let _ = app.emit(
|
||||||
|
"tray://set_powerplan",
|
||||||
|
PowerPlanMode::HighPerformance as i32,
|
||||||
|
);
|
||||||
|
// let _ = power_plan_high.set_checked(true);
|
||||||
|
}
|
||||||
|
"power_plan_balanced" => {
|
||||||
|
let _ = app.emit("tray://set_powerplan", PowerPlanMode::Balanced as i32);
|
||||||
|
// let _ = power_plan_balanced.set_checked(true);
|
||||||
|
}
|
||||||
|
"power_plan_powersave" => {
|
||||||
|
let _ = app.emit("tray://set_powerplan", PowerPlanMode::PowerSaving as i32);
|
||||||
|
// let _ = power_plan_powersave.set_checked(true);
|
||||||
|
}
|
||||||
|
id if id.starts_with("launch_option_") => {
|
||||||
|
// 提取索引
|
||||||
|
if let Ok(index) = id.replace("launch_option_", "").parse::<usize>() {
|
||||||
|
let _ = app.emit("tray://set_launch_index", index);
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
})
|
})
|
||||||
.on_tray_icon_event(|tray, event| {
|
.on_tray_icon_event(|tray, event| {
|
||||||
@@ -37,68 +242,3 @@ pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tray Menu
|
|
||||||
// let quit = CustomMenuItem::new("quit".to_string(), "Quit");
|
|
||||||
// let hide = CustomMenuItem::new("hide".to_string(), "Hide");
|
|
||||||
// let tray_menu = SystemTrayMenu::new() // insert the menu items here
|
|
||||||
// .add_item(hide)
|
|
||||||
// .add_item(quit);
|
|
||||||
// .add_native_item(SystemTrayMenuItem::Separator)
|
|
||||||
// let toggle = MenuItemBuilder::with_id("toggle", "Toggle").build(app)?;
|
|
||||||
// let menu = MenuBuilder::new(app).items(&[&toggle]).build()?;
|
|
||||||
|
|
||||||
// Setup Tray
|
|
||||||
// let tray = tauri::tray::TrayIconBuilder::with_id("my-tray").build(app)?;
|
|
||||||
// let _ = TrayIconBuilder::new()
|
|
||||||
// .menu(&menu)
|
|
||||||
// .on_menu_event(move |_, event| {
|
|
||||||
// match event.id().as_ref() {
|
|
||||||
// "toggle" => {
|
|
||||||
// println!("toggle clicked");
|
|
||||||
// }
|
|
||||||
// _ => (),
|
|
||||||
// }
|
|
||||||
// // match event {
|
|
||||||
// // SystemTrayEvent::LeftClick { position: _, size: _, .. } => {
|
|
||||||
// // let window = app.get_window("main").unwrap();
|
|
||||||
// // window.show().unwrap();
|
|
||||||
// // window.set_focus().unwrap();
|
|
||||||
|
|
||||||
// // // thread::sleep(Duration::from_millis(100));
|
|
||||||
// // // window.set_always_on_top(false).unwrap();
|
|
||||||
// // println!("system tray received a left click");
|
|
||||||
// // }
|
|
||||||
// // SystemTrayEvent::RightClick { position: _, size: _, .. } => {
|
|
||||||
// // // let window = app.get_window("main").unwrap();
|
|
||||||
// // // window.hide().unwrap();
|
|
||||||
// // println!("system tray received a right click");
|
|
||||||
// // }
|
|
||||||
// // SystemTrayEvent::DoubleClick { position: _, size: _, .. } => {
|
|
||||||
// // println!("system tray received a double click");
|
|
||||||
// // }
|
|
||||||
// // SystemTrayEvent::MenuItemClick { id, .. } =>
|
|
||||||
// // match id.as_str() {
|
|
||||||
// // "quit" => {
|
|
||||||
// // std::process::exit(0);
|
|
||||||
// // }
|
|
||||||
// // "hide" => {
|
|
||||||
// // let window = app.get_window("main").unwrap();
|
|
||||||
// // window.hide().unwrap();
|
|
||||||
// // }
|
|
||||||
// // _ => {}
|
|
||||||
// // }
|
|
||||||
// // _ => {}
|
|
||||||
// // }
|
|
||||||
// })
|
|
||||||
// .on_tray_icon_event(|tray, event| {
|
|
||||||
// if event.click_type == ClickType::Left {
|
|
||||||
// let app = tray.app_handle();
|
|
||||||
// if let Some(webview_window) = app.get_webview_window("main") {
|
|
||||||
// let _ = webview_window.show();
|
|
||||||
// let _ = webview_window.set_focus();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .build(app)
|
|
||||||
// .unwrap();
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
pub fn to_json(vdf_data: &str) -> String {
|
pub fn to_json(vdf_data: &str) -> String {
|
||||||
let linebreak = match std::env::consts::OS {
|
let linebreak = match std::env::consts::OS {
|
||||||
"macos" => "\r",
|
"macos" => "\n", //"\r",
|
||||||
"windows" => "\n",
|
"windows" => "\n",
|
||||||
"linux" => "\n",
|
"linux" => "\n",
|
||||||
_ => "\n",
|
_ => "\n",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// NOTE: 这样会跳过顶层{}
|
||||||
let startpoint = vdf_data.find('{').unwrap_or(0);
|
let startpoint = vdf_data.find('{').unwrap_or(0);
|
||||||
let vdf_data = &vdf_data[startpoint..];
|
let vdf_data = &vdf_data[startpoint..];
|
||||||
|
|
||||||
@@ -31,49 +32,107 @@ pub fn to_json(vdf_data: &str) -> String {
|
|||||||
json_data.push_str(&line);
|
json_data.push_str(&line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// let json_str = json_data
|
||||||
json_data = json_data
|
json_data = json_data
|
||||||
.replace(",}", "}")
|
.replace(",}", "}")
|
||||||
.trim_start_matches(": ")
|
.trim_start_matches(": ")
|
||||||
.trim_end_matches(',')
|
.trim_end_matches(',')
|
||||||
.to_string();
|
.to_string();
|
||||||
|
// json_data = format!("{{{}}}", json_str);
|
||||||
|
|
||||||
json_data
|
return json_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_vdf(json_data: &str) -> String {
|
||||||
|
let json_value: serde_json::Value = serde_json::from_str(json_data).unwrap();
|
||||||
|
let mut vdf_data = String::new();
|
||||||
|
build_vdf(&json_value, &mut vdf_data, 0);
|
||||||
|
vdf_data
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_vdf(json_value: &serde_json::Value, vdf_data: &mut String, indent_level: usize) {
|
||||||
|
match json_value {
|
||||||
|
serde_json::Value::Object(obj) => {
|
||||||
|
for (key, value) in obj {
|
||||||
|
vdf_data.push_str(&"\t".repeat(indent_level));
|
||||||
|
vdf_data.push_str(&format!("\"{}\"\n", key));
|
||||||
|
vdf_data.push_str(&"\t".repeat(indent_level));
|
||||||
|
vdf_data.push_str("{\n");
|
||||||
|
build_vdf(value, vdf_data, indent_level + 1);
|
||||||
|
vdf_data.push_str(&"\t".repeat(indent_level));
|
||||||
|
vdf_data.push_str("}\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serde_json::Value::String(s) => {
|
||||||
|
vdf_data.push_str(&"\t".repeat(indent_level));
|
||||||
|
vdf_data.push_str(&format!("\"{}\"\t\t\"{}\"\n", s, s));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
vdf_data.push_str(&"\t".repeat(indent_level));
|
||||||
|
vdf_data.push_str(&format!("\"{}\"\t\t\"{}\"\n", json_value, json_value));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
|
||||||
|
static VDF_DATA: &str = r#""users"
|
||||||
|
{
|
||||||
|
"76561198315078806"
|
||||||
|
{
|
||||||
|
"AccountName" "_jerry_dota2"
|
||||||
|
"PersonaName" "Rop紫(已黑化)"
|
||||||
|
"RememberPassword" "1"
|
||||||
|
"WantsOfflineMode" "0"
|
||||||
|
"SkipOfflineModeWarning" "0"
|
||||||
|
"AllowAutoLogin" "1"
|
||||||
|
"MostRecent" "1"
|
||||||
|
"Timestamp" "1742706884"
|
||||||
|
}
|
||||||
|
"76561198107125441"
|
||||||
|
{
|
||||||
|
"AccountName" "_im_ai_"
|
||||||
|
"PersonaName" "Buongiorno"
|
||||||
|
"RememberPassword" "1"
|
||||||
|
"WantsOfflineMode" "0"
|
||||||
|
"SkipOfflineModeWarning" "0"
|
||||||
|
"AllowAutoLogin" "1"
|
||||||
|
"MostRecent" "0"
|
||||||
|
"Timestamp" "1739093763"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
static JSON_DATA: &str = r#"{
|
||||||
|
"users": {
|
||||||
|
"76561198315078806": {
|
||||||
|
"AccountName": "_jerry_dota2",
|
||||||
|
"PersonaName": "Rop紫(已黑化)",
|
||||||
|
"RememberPassword": "1",
|
||||||
|
"WantsOfflineMode": "0",
|
||||||
|
"SkipOfflineModeWarning": "0",
|
||||||
|
"AllowAutoLogin": "1",
|
||||||
|
"MostRecent": "1",
|
||||||
|
"Timestamp": "1742706884"
|
||||||
|
},
|
||||||
|
"76561198107125441": {
|
||||||
|
"AccountName": "_im_ai_",
|
||||||
|
"PersonaName": "Buongiorno",
|
||||||
|
"RememberPassword": "1",
|
||||||
|
"WantsOfflineMode": "0",
|
||||||
|
"SkipOfflineModeWarning": "0",
|
||||||
|
"AllowAutoLogin": "1",
|
||||||
|
"MostRecent": "0",
|
||||||
|
"Timestamp": "1739093763"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_to_json() {
|
fn test_to_json() {
|
||||||
let vdf_data = "\"users\"
|
|
||||||
{
|
|
||||||
\"76561198315078806\"
|
|
||||||
{
|
|
||||||
\"AccountName\" \"_jerry_dota2\"
|
|
||||||
\"PersonaName\" \"Rop紫(已黑化)\"
|
|
||||||
\"RememberPassword\" \"1\"
|
|
||||||
\"WantsOfflineMode\" \"0\"
|
|
||||||
\"SkipOfflineModeWarning\" \"0\"
|
|
||||||
\"AllowAutoLogin\" \"1\"
|
|
||||||
\"MostRecent\" \"1\"
|
|
||||||
\"Timestamp\" \"1742706884\"
|
|
||||||
}
|
|
||||||
\"76561198107125441\"
|
|
||||||
{
|
|
||||||
\"AccountName\" \"_im_ai_\"
|
|
||||||
\"PersonaName\" \"Buongiorno\"
|
|
||||||
\"RememberPassword\" \"1\"
|
|
||||||
\"WantsOfflineMode\" \"0\"
|
|
||||||
\"SkipOfflineModeWarning\" \"0\"
|
|
||||||
\"AllowAutoLogin\" \"1\"
|
|
||||||
\"MostRecent\" \"0\"
|
|
||||||
\"Timestamp\" \"1739093763\"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
";
|
|
||||||
|
|
||||||
// let expected_json = r#"{"key1": "value1","key2": "value2","subkey": {"key3": "value3"}}"#;
|
// let expected_json = r#"{"key1": "value1","key2": "value2","subkey": {"key3": "value3"}}"#;
|
||||||
let json_data = to_json(vdf_data);
|
let json_data = super::to_json(VDF_DATA);
|
||||||
|
println!("{}", json_data);
|
||||||
|
|
||||||
// 解析json
|
// 解析json
|
||||||
let json_value: serde_json::Value = serde_json::from_str(&json_data).unwrap();
|
let json_value: serde_json::Value = serde_json::from_str(&json_data).unwrap();
|
||||||
@@ -81,4 +140,12 @@ mod tests {
|
|||||||
println!("{}", json_value)
|
println!("{}", json_value)
|
||||||
// assert_eq!(to_json(vdf_data), expected_json);
|
// assert_eq!(to_json(vdf_data), expected_json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_to_vdf() {
|
||||||
|
// let json_data = r#"{"key1": "value1","key2": "value2","subkey": {"key3": "value3"}}"#;
|
||||||
|
let vdf_data = super::to_vdf(JSON_DATA);
|
||||||
|
|
||||||
|
println!("{}", vdf_data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use base64::engine::general_purpose::STANDARD;
|
use base64::engine::general_purpose::STANDARD;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use tauri_plugin_http::reqwest::blocking::get;
|
use tauri_plugin_http::reqwest::
|
||||||
|
|
||||||
|
blocking::get;
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
use crate::steam;
|
use crate::steam;
|
||||||
@@ -44,6 +47,81 @@ pub struct LocalUser {
|
|||||||
avatar_key: String,
|
avatar_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct VideoConfig {
|
||||||
|
version: String,
|
||||||
|
vendor_id: String,
|
||||||
|
device_id: String,
|
||||||
|
cpu_level: String,
|
||||||
|
gpu_mem_level: String,
|
||||||
|
gpu_level: String,
|
||||||
|
knowndevice: String,
|
||||||
|
defaultres: String,
|
||||||
|
defaultresheight: String,
|
||||||
|
refreshrate_numerator: String,
|
||||||
|
refreshrate_denominator: String,
|
||||||
|
fullscreen: String,
|
||||||
|
coop_fullscreen: String,
|
||||||
|
nowindowborder: String,
|
||||||
|
mat_vsync: String,
|
||||||
|
fullscreen_min_on_focus_loss: String,
|
||||||
|
high_dpi: String,
|
||||||
|
auto_config: String,
|
||||||
|
shaderquality: String,
|
||||||
|
r_texturefilteringquality: String,
|
||||||
|
msaa_samples: String,
|
||||||
|
r_csgo_cmaa_enable: String,
|
||||||
|
videocfg_shadow_quality: String,
|
||||||
|
videocfg_dynamic_shadows: String,
|
||||||
|
videocfg_texture_detail: String,
|
||||||
|
videocfg_particle_detail: String,
|
||||||
|
videocfg_ao_detail: String,
|
||||||
|
videocfg_hdr_detail: String,
|
||||||
|
videocfg_fsr_detail: String,
|
||||||
|
monitor_index: String,
|
||||||
|
r_low_latency: String,
|
||||||
|
aspectratiomode: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for VideoConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
VideoConfig {
|
||||||
|
version: "15".to_string(),
|
||||||
|
vendor_id: "0".to_string(),
|
||||||
|
device_id: "0".to_string(),
|
||||||
|
cpu_level: "3".to_string(),
|
||||||
|
gpu_mem_level: "3".to_string(),
|
||||||
|
gpu_level: "3".to_string(),
|
||||||
|
knowndevice: "0".to_string(),
|
||||||
|
defaultres: "1920".to_string(),
|
||||||
|
defaultresheight: "1080".to_string(),
|
||||||
|
refreshrate_numerator: "144".to_string(),
|
||||||
|
refreshrate_denominator: "1".to_string(),
|
||||||
|
fullscreen: "1".to_string(),
|
||||||
|
coop_fullscreen: "0".to_string(),
|
||||||
|
nowindowborder: "1".to_string(),
|
||||||
|
mat_vsync: "0".to_string(),
|
||||||
|
fullscreen_min_on_focus_loss: "1".to_string(),
|
||||||
|
high_dpi: "0".to_string(),
|
||||||
|
auto_config: "2".to_string(),
|
||||||
|
shaderquality: "0".to_string(),
|
||||||
|
r_texturefilteringquality: "3".to_string(),
|
||||||
|
msaa_samples: "2".to_string(),
|
||||||
|
r_csgo_cmaa_enable: "0".to_string(),
|
||||||
|
videocfg_shadow_quality: "0".to_string(),
|
||||||
|
videocfg_dynamic_shadows: "1".to_string(),
|
||||||
|
videocfg_texture_detail: "1".to_string(),
|
||||||
|
videocfg_particle_detail: "0".to_string(),
|
||||||
|
videocfg_ao_detail: "0".to_string(),
|
||||||
|
videocfg_hdr_detail: "3".to_string(),
|
||||||
|
videocfg_fsr_detail: "0".to_string(),
|
||||||
|
monitor_index: "0".to_string(),
|
||||||
|
r_low_latency: "1".to_string(),
|
||||||
|
aspectratiomode: "0".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse_login_users(steam_dir: &str) -> Result<Vec<LoginUser>> {
|
pub fn parse_login_users(steam_dir: &str) -> Result<Vec<LoginUser>> {
|
||||||
let t_path = Path::new(steam_dir).join("config/loginusers.vdf");
|
let t_path = Path::new(steam_dir).join("config/loginusers.vdf");
|
||||||
if !t_path.exists() {
|
if !t_path.exists() {
|
||||||
@@ -56,7 +134,10 @@ pub fn parse_login_users(steam_dir: &str) -> Result<Vec<LoginUser>> {
|
|||||||
|
|
||||||
let mut users = Vec::new();
|
let mut users = Vec::new();
|
||||||
for (k, v) in kv {
|
for (k, v) in kv {
|
||||||
let props = v.as_object().unwrap();
|
let props = match v.as_object() {
|
||||||
|
Some(p) => p,
|
||||||
|
None => continue, // 跳过非对象类型的值
|
||||||
|
};
|
||||||
|
|
||||||
let avatar = if let Some(img) = read_avatar(&steam_dir, &k) {
|
let avatar = if let Some(img) = read_avatar(&steam_dir, &k) {
|
||||||
img
|
img
|
||||||
@@ -64,7 +145,11 @@ pub fn parse_login_users(steam_dir: &str) -> Result<Vec<LoginUser>> {
|
|||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let id64 = k.parse::<u64>()?;
|
// 跳过无法解析为 u64 的键
|
||||||
|
let id64 = match k.parse::<u64>() {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
|
||||||
let user = LoginUser {
|
let user = LoginUser {
|
||||||
steam_id32: steam::id::id64_to_32(id64),
|
steam_id32: steam::id::id64_to_32(id64),
|
||||||
@@ -138,7 +223,11 @@ pub fn parse_local_users(steam_dir: &str) -> Result<Vec<LocalUser>> {
|
|||||||
|
|
||||||
// 只处理目录
|
// 只处理目录
|
||||||
if entry.file_type().is_dir() {
|
if entry.file_type().is_dir() {
|
||||||
let id = path.file_name().unwrap().to_str().unwrap();
|
// 安全获取文件名
|
||||||
|
let id = match path.file_name().and_then(|n| n.to_str()) {
|
||||||
|
Some(id_str) => id_str,
|
||||||
|
None => continue, // 跳过无法获取文件名的路径
|
||||||
|
};
|
||||||
|
|
||||||
// 检查 localconfig.vdf 文件是否存在
|
// 检查 localconfig.vdf 文件是否存在
|
||||||
let local_config_path = path.join("config/localconfig.vdf");
|
let local_config_path = path.join("config/localconfig.vdf");
|
||||||
@@ -146,18 +235,26 @@ pub fn parse_local_users(steam_dir: &str) -> Result<Vec<LocalUser>> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取并解析 localconfig.vdf 文件
|
// 读取并解析 localconfig.vdf 文件,如果失败则跳过
|
||||||
let data = fs::read_to_string(local_config_path)?;
|
let data = match fs::read_to_string(&local_config_path) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => continue, // 跳过无法读取的文件
|
||||||
|
};
|
||||||
|
|
||||||
let json_data = super::parse::to_json(&data);
|
let json_data = super::parse::to_json(&data);
|
||||||
let kv: HashMap<String, Value> = serde_json::from_str(&json_data)?;
|
let kv = match serde_json::from_str::<HashMap<String, Value>>(&json_data) {
|
||||||
|
Ok(kv) => kv,
|
||||||
|
Err(_) => continue, // 跳过无法解析的 JSON
|
||||||
|
};
|
||||||
|
|
||||||
|
// 剥离顶层 UserLocalConfigStore
|
||||||
|
// let kv = kv.get("UserLocalConfigStore").and_then(|v| v.as_object()).unwrap();
|
||||||
|
|
||||||
// 获取 friends 节点
|
// 获取 friends 节点
|
||||||
let friends = kv.get("friends").and_then(|v| v.as_object());
|
let friends = match kv.get("friends").and_then(|v| v.as_object()) {
|
||||||
if friends.is_none() {
|
Some(f) => f,
|
||||||
continue;
|
None => continue,
|
||||||
}
|
};
|
||||||
|
|
||||||
let friends = friends.unwrap();
|
|
||||||
|
|
||||||
// 获取 PersonaName
|
// 获取 PersonaName
|
||||||
let persona_name = friends
|
let persona_name = friends
|
||||||
@@ -175,9 +272,15 @@ pub fn parse_local_users(steam_dir: &str) -> Result<Vec<LocalUser>> {
|
|||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
|
// 安全解析 ID,如果失败则跳过
|
||||||
|
let steam_id32 = match id.parse::<u32>() {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(_) => continue, // 跳过无法解析为 u32 的 ID
|
||||||
|
};
|
||||||
|
|
||||||
// 创建 LocalUser 并加入列表
|
// 创建 LocalUser 并加入列表
|
||||||
local_users.push(LocalUser {
|
local_users.push(LocalUser {
|
||||||
steam_id32: id.parse::<u32>().unwrap(),
|
steam_id32,
|
||||||
persona_name,
|
persona_name,
|
||||||
avatar_key,
|
avatar_key,
|
||||||
});
|
});
|
||||||
@@ -270,6 +373,184 @@ fn read_avatar(steam_dir: &str, steam_id64: &str) -> Option<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_cs2_video(file_path: &str) -> Result<VideoConfig> {
|
||||||
|
// TODO: no kv
|
||||||
|
let data = fs::read_to_string(file_path)?;
|
||||||
|
let json_data = super::parse::to_json(&data);
|
||||||
|
let kv: HashMap<String, String> = serde_json::from_str(&json_data)?;
|
||||||
|
let video_config = VideoConfig {
|
||||||
|
version: kv.get("Version").unwrap_or(&"".to_string()).to_string(),
|
||||||
|
vendor_id: kv.get("VendorID").unwrap_or(&"".to_string()).to_string(),
|
||||||
|
device_id: kv.get("DeviceID").unwrap_or(&"".to_string()).to_string(),
|
||||||
|
cpu_level: kv
|
||||||
|
.get("setting.cpu_level")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
gpu_mem_level: kv
|
||||||
|
.get("setting.gpu_mem_level")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
gpu_level: kv
|
||||||
|
.get("setting.gpu_level")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
knowndevice: kv
|
||||||
|
.get("setting.knowndevice")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
defaultres: kv
|
||||||
|
.get("setting.defaultres")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
defaultresheight: kv
|
||||||
|
.get("setting.defaultresheight")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
refreshrate_numerator: kv
|
||||||
|
.get("setting.refreshrate_numerator")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
refreshrate_denominator: kv
|
||||||
|
.get("setting.refreshrate_denominator")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
fullscreen: kv
|
||||||
|
.get("setting.fullscreen")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
coop_fullscreen: kv
|
||||||
|
.get("setting.coop_fullscreen")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
nowindowborder: kv
|
||||||
|
.get("setting.nowindowborder")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
mat_vsync: kv
|
||||||
|
.get("setting.mat_vsync")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
fullscreen_min_on_focus_loss: kv
|
||||||
|
.get("setting.fullscreen_min_on_focus_loss")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
high_dpi: kv
|
||||||
|
.get("setting.high_dpi")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
auto_config: kv.get("AutoConfig").unwrap_or(&"".to_string()).to_string(),
|
||||||
|
shaderquality: kv
|
||||||
|
.get("setting.shaderquality")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
r_texturefilteringquality: kv
|
||||||
|
.get("setting.r_texturefilteringquality")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
msaa_samples: kv
|
||||||
|
.get("setting.msaa_samples")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
r_csgo_cmaa_enable: kv
|
||||||
|
.get("setting.r_csgo_cmaa_enable")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
videocfg_shadow_quality: kv
|
||||||
|
.get("setting.videocfg_shadow_quality")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
videocfg_dynamic_shadows: kv
|
||||||
|
.get("setting.videocfg_dynamic_shadows")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
videocfg_texture_detail: kv
|
||||||
|
.get("setting.videocfg_texture_detail")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
videocfg_particle_detail: kv
|
||||||
|
.get("setting.videocfg_particle_detail")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
videocfg_ao_detail: kv
|
||||||
|
.get("setting.videocfg_ao_detail")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
videocfg_hdr_detail: kv
|
||||||
|
.get("setting.videocfg_hdr_detail")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
videocfg_fsr_detail: kv
|
||||||
|
.get("setting.videocfg_fsr_detail")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
monitor_index: kv
|
||||||
|
.get("setting.monitor_index")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
r_low_latency: kv
|
||||||
|
.get("setting.r_low_latency")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
aspectratiomode: kv
|
||||||
|
.get("setting.aspectratiomode")
|
||||||
|
.unwrap_or(&"".to_string())
|
||||||
|
.to_string(),
|
||||||
|
};
|
||||||
|
Ok(video_config)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_cs2_video(file_path: &str, data: VideoConfig) -> Result<()> {
|
||||||
|
// 读取文件内容
|
||||||
|
let file_content = fs::read_to_string(file_path)?;
|
||||||
|
|
||||||
|
// 定义正则表达式匹配模式
|
||||||
|
let re = Regex::new(r#""(setting\.\w+)"\s+"-?\d+""#).unwrap();
|
||||||
|
|
||||||
|
// 替换字段值
|
||||||
|
let updated_content = re.replace_all(&file_content, |caps: ®ex::Captures| {
|
||||||
|
let key = &caps[1]; // 捕获的键名
|
||||||
|
let value = match key {
|
||||||
|
"Version" => &data.version,
|
||||||
|
"VendorID" => &data.vendor_id,
|
||||||
|
"DeviceID" => &data.device_id,
|
||||||
|
"setting.cpu_level" => &data.cpu_level,
|
||||||
|
"setting.gpu_mem_level" => &data.gpu_mem_level,
|
||||||
|
"setting.gpu_level" => &data.gpu_level,
|
||||||
|
"setting.knowndevice" => &data.knowndevice,
|
||||||
|
"setting.defaultres" => &data.defaultres,
|
||||||
|
"setting.defaultresheight" => &data.defaultresheight,
|
||||||
|
"setting.refreshrate_numerator" => &data.refreshrate_numerator,
|
||||||
|
"setting.refreshrate_denominator" => &data.refreshrate_denominator,
|
||||||
|
"setting.fullscreen" => &data.fullscreen,
|
||||||
|
"setting.coop_fullscreen" => &data.coop_fullscreen,
|
||||||
|
"setting.nowindowborder" => &data.nowindowborder,
|
||||||
|
"setting.mat_vsync" => &data.mat_vsync,
|
||||||
|
"setting.fullscreen_min_on_focus_loss" => &data.fullscreen_min_on_focus_loss,
|
||||||
|
"setting.high_dpi" => &data.high_dpi,
|
||||||
|
"AutoConfig" => &data.auto_config,
|
||||||
|
"setting.shaderquality" => &data.shaderquality,
|
||||||
|
"setting.r_texturefilteringquality" => &data.r_texturefilteringquality,
|
||||||
|
"setting.msaa_samples" => &data.msaa_samples,
|
||||||
|
"setting.r_csgo_cmaa_enable" => &data.r_csgo_cmaa_enable,
|
||||||
|
"setting.videocfg_shadow_quality" => &data.videocfg_shadow_quality,
|
||||||
|
"setting.videocfg_dynamic_shadows" => &data.videocfg_dynamic_shadows,
|
||||||
|
"setting.videocfg_texture_detail" => &data.videocfg_texture_detail,
|
||||||
|
"setting.videocfg_particle_detail" => &data.videocfg_particle_detail,
|
||||||
|
"setting.videocfg_ao_detail" => &data.videocfg_ao_detail,
|
||||||
|
"setting.videocfg_hdr_detail" => &data.videocfg_hdr_detail,
|
||||||
|
"setting.videocfg_fsr_detail" => &data.videocfg_fsr_detail,
|
||||||
|
"setting.monitor_index" => &data.monitor_index,
|
||||||
|
"setting.r_low_latency" => &data.r_low_latency,
|
||||||
|
"setting.aspectratiomode" => &data.aspectratiomode,
|
||||||
|
_ => "", // 默认情况
|
||||||
|
};
|
||||||
|
format!(r#""{}" "{}""#, key, value)
|
||||||
|
});
|
||||||
|
|
||||||
|
fs::write(file_path, updated_content.as_ref())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -279,4 +560,25 @@ mod tests {
|
|||||||
let users = get_users("D:\\Programs\\Steam").unwrap();
|
let users = get_users("D:\\Programs\\Steam").unwrap();
|
||||||
println!("{:?}", users);
|
println!("{:?}", users);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_cs2_video() {
|
||||||
|
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
||||||
|
let file_path = format!("{}/src/vdf/tests/cs2_video.txt", manifest_dir);
|
||||||
|
let video_config = get_cs2_video(&file_path).unwrap();
|
||||||
|
println!("{:?}", video_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_set_cs2_video() {
|
||||||
|
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
||||||
|
let file_path = format!("{}/temp/cs2_video.txt", manifest_dir);
|
||||||
|
fs::copy(
|
||||||
|
format!("{}/src/vdf/tests/cs2_video.txt", manifest_dir),
|
||||||
|
file_path.clone(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let video_config = VideoConfig::default();
|
||||||
|
set_cs2_video(&file_path, video_config).unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
"copyright": "",
|
"copyright": "",
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
"externalBin": [],
|
"externalBin": [],
|
||||||
|
"resources": [
|
||||||
|
"resources/csda.exe"
|
||||||
|
],
|
||||||
|
"createUpdaterArtifacts": true,
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
@@ -22,7 +26,14 @@
|
|||||||
"windows": {
|
"windows": {
|
||||||
"certificateThumbprint": null,
|
"certificateThumbprint": null,
|
||||||
"digestAlgorithm": "sha256",
|
"digestAlgorithm": "sha256",
|
||||||
"timestampUrl": ""
|
"timestampUrl": "",
|
||||||
|
"nsis": {
|
||||||
|
"languages": [
|
||||||
|
"SimpChinese",
|
||||||
|
"English",
|
||||||
|
"TradChinese"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"longDescription": "",
|
"longDescription": "",
|
||||||
"macOS": {
|
"macOS": {
|
||||||
@@ -32,7 +43,6 @@
|
|||||||
"providerShortName": null,
|
"providerShortName": null,
|
||||||
"signingIdentity": null
|
"signingIdentity": null
|
||||||
},
|
},
|
||||||
"resources": [],
|
|
||||||
"shortDescription": "",
|
"shortDescription": "",
|
||||||
"linux": {
|
"linux": {
|
||||||
"deb": {
|
"deb": {
|
||||||
@@ -42,7 +52,7 @@
|
|||||||
},
|
},
|
||||||
"productName": "CS工具箱",
|
"productName": "CS工具箱",
|
||||||
"mainBinaryName": "cstb",
|
"mainBinaryName": "cstb",
|
||||||
"version": "0.0.5-beta.2",
|
"version": "0.0.7-beta.1",
|
||||||
"identifier": "upup.cool",
|
"identifier": "upup.cool",
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"deep-link": {
|
"deep-link": {
|
||||||
@@ -51,6 +61,23 @@
|
|||||||
"cstb"
|
"cstb"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"cli": {
|
||||||
|
"description": "CS Toolbox CLI",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "hidden",
|
||||||
|
"description": "hidden on start"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"updater": {
|
||||||
|
"active": true,
|
||||||
|
"endpoints": [
|
||||||
|
"https://gh-info.okk.cool/repos/plsgo/cstb/releases/latest/pre/tauri"
|
||||||
|
],
|
||||||
|
"dialog": true,
|
||||||
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEU3QUY5MUU5OTc3N0FCODkKUldTSnEzZVg2Wkd2NTRlVDBxVWNoYkNxZ1c1TlVJT0QwYkFOcFVPUnRQTGlmTVdRcVRRRUdlMUoK"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
@@ -70,7 +97,8 @@
|
|||||||
"transparent": true,
|
"transparent": true,
|
||||||
"theme": null,
|
"theme": null,
|
||||||
"hiddenTitle": true,
|
"hiddenTitle": true,
|
||||||
"titleBarStyle": "Transparent"
|
"titleBarStyle": "Transparent",
|
||||||
|
"visible": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import { Chip, Code, Skeleton } from "@heroui/react"
|
|||||||
import useSWR from "swr"
|
import useSWR from "swr"
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <CfgxList />
|
return (
|
||||||
|
<section className="flex flex-col gap-4 overflow-hidden">
|
||||||
|
<CfgxList />
|
||||||
|
</section>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CfgxList() {
|
function CfgxList() {
|
||||||
|
|||||||
126
src/app/(main)/dynamic/page.tsx
Normal file
126
src/app/(main)/dynamic/page.tsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState } from "react"
|
||||||
|
import { MarkdownRender } from "@/components/markdown"
|
||||||
|
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, 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">
|
||||||
|
<CardHeader>
|
||||||
|
<CardIcon>
|
||||||
|
<NewspaperFolding /> 动态
|
||||||
|
</CardIcon>
|
||||||
|
<CardTool>
|
||||||
|
<Tabs
|
||||||
|
selectedKey={selectedKey}
|
||||||
|
onSelectionChange={setSelectedKey}
|
||||||
|
size="sm"
|
||||||
|
radius="full"
|
||||||
|
classNames={{
|
||||||
|
base: "min-w-0",
|
||||||
|
tabList: "gap-0 p-0",
|
||||||
|
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 showTestVersions={showTestVersions} />
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReleaseNotes = ({ showTestVersions }: { showTestVersions: boolean }) => {
|
||||||
|
const noticeFetcher = async () => {
|
||||||
|
const supabase = createClient()
|
||||||
|
let query = supabase
|
||||||
|
.from("ReleaseNote")
|
||||||
|
.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?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"
|
||||||
|
>
|
||||||
|
{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,76 +1,426 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import {
|
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "@/components/window/Card"
|
||||||
Card,
|
|
||||||
CardBody,
|
|
||||||
CardHeader,
|
|
||||||
CardIcon,
|
|
||||||
CardTool,
|
|
||||||
} from "@/components/window/Card"
|
|
||||||
import { ToolButton } from "@/components/window/ToolButton"
|
import { ToolButton } from "@/components/window/ToolButton"
|
||||||
import { Chip } from "@heroui/react"
|
import { Chip, Skeleton } from "@heroui/react"
|
||||||
import { Refresh, SettingConfig } from "@icon-park/react"
|
import { Refresh, SettingConfig } from "@icon-park/react"
|
||||||
// import { version } from "@tauri-apps/plugin-os"
|
import { useHardwareStore } from "@/store/hardware"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { type AllSystemInfo, allSysInfo } from "tauri-plugin-system-info-api"
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<Card className="h-full">
|
<section className="flex flex-col gap-4 overflow-hidden rounded-lg">
|
||||||
<CardHeader>
|
<div className="flex flex-col h-full gap-4 overflow-hidden hide-scrollbar">
|
||||||
<CardIcon type="menu">
|
<Card className="overflow-hidden">
|
||||||
<SettingConfig /> 硬件
|
<CardHeader>
|
||||||
</CardIcon>
|
<CardIcon type="menu">
|
||||||
<CardTool>
|
<SettingConfig /> 硬件
|
||||||
{/* <ToolButton>
|
</CardIcon>
|
||||||
<UploadOne />
|
<CardTool>
|
||||||
云同步
|
{/* <ToolButton>
|
||||||
</ToolButton> */}
|
<UploadOne />
|
||||||
<ToolButton>
|
云同步
|
||||||
<Refresh /> 刷新
|
</ToolButton> */}
|
||||||
</ToolButton>
|
<HardwareInfo />
|
||||||
</CardTool>
|
</CardTool>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<HardwareInfo />
|
<HardwareInfoContent />
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function HardwareInfo() {
|
function HardwareInfo() {
|
||||||
const [allSysData, setAllSysData] = useState<AllSystemInfo>()
|
const { refreshHardwareInfo } = useHardwareStore()
|
||||||
// const [memInfo, setMemInfo] = useState("")
|
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||||
// const [staticData, setStaticData] = useState("")
|
|
||||||
// const [cpuData, setCpuData] = useState("")
|
|
||||||
// const [batteryData, setBatteryData] = useState("")
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
const sys = await allSysInfo()
|
|
||||||
console.log(sys)
|
|
||||||
setAllSysData(sys)
|
|
||||||
// console.log(await memoryInfo())
|
|
||||||
// console.log(await staticInfo())
|
|
||||||
// console.log(await cpuInfo())
|
|
||||||
// console.log(await batteries())
|
|
||||||
}
|
|
||||||
|
|
||||||
void fetchData()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1.5">
|
<ToolButton
|
||||||
<Chip>CPU型号: {allSysData?.cpus[0]?.brand}</Chip>
|
onClick={async () => {
|
||||||
<Chip>线程数: {allSysData?.cpu_count}</Chip>
|
setIsRefreshing(true)
|
||||||
<Chip>
|
try {
|
||||||
系统: {allSysData?.name} {allSysData?.os_version}
|
await refreshHardwareInfo()
|
||||||
</Chip>
|
} finally {
|
||||||
<Chip>
|
setIsRefreshing(false)
|
||||||
内存:
|
}
|
||||||
{allSysData?.total_memory &&
|
}}
|
||||||
`${(allSysData.total_memory / 1024 / 1024 / 1024).toFixed(0)}GB`}
|
disabled={isRefreshing}
|
||||||
</Chip>
|
>
|
||||||
|
<Refresh /> 刷新
|
||||||
|
</ToolButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function HardwareInfoContent() {
|
||||||
|
const { state, fetchHardwareInfo } = useHardwareStore()
|
||||||
|
const [isLoading, setIsLoading] = useState(!state.allSysData)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 如果数据不存在,则加载数据
|
||||||
|
if (!state.allSysData) {
|
||||||
|
setIsLoading(true)
|
||||||
|
void fetchHardwareInfo().finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []) // 只在组件挂载时执行一次,state 是响应式的,不需要作为依赖
|
||||||
|
|
||||||
|
const allSysData = state.allSysData
|
||||||
|
const computerInfo = state.computerInfo || {}
|
||||||
|
const gpuInfo = state.gpuInfo
|
||||||
|
const memoryInfo = state.memoryInfo || []
|
||||||
|
const monitorInfo = state.monitorInfo || []
|
||||||
|
const motherboardInfo = state.motherboardInfo
|
||||||
|
|
||||||
|
const formatBytes = (bytes?: number) => {
|
||||||
|
if (!bytes) return "未知"
|
||||||
|
const gb = bytes / 1024 / 1024 / 1024
|
||||||
|
if (gb >= 1) {
|
||||||
|
return `${gb.toFixed(2)}GB`
|
||||||
|
}
|
||||||
|
const mb = bytes / 1024 / 1024
|
||||||
|
return `${mb.toFixed(2)}MB`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化系统信息:Windows 11 (26200) 25H2
|
||||||
|
const formatSystemInfo = () => {
|
||||||
|
const osVersion = allSysData?.os_version || null
|
||||||
|
// 使用 OSDisplayVersion 作为版本代码(如 "25H2")
|
||||||
|
const osDisplayVersion = computerInfo.OSDisplayVersion || null
|
||||||
|
|
||||||
|
let systemStr = allSysData?.name || "未知"
|
||||||
|
if (osVersion) {
|
||||||
|
systemStr += ` ${osVersion}`
|
||||||
|
}
|
||||||
|
if (osDisplayVersion) {
|
||||||
|
systemStr += ` ${osDisplayVersion}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemStr
|
||||||
|
}
|
||||||
|
const memoryUsagePercent =
|
||||||
|
allSysData?.total_memory && allSysData?.used_memory !== undefined
|
||||||
|
? Math.round((allSysData.used_memory / allSysData.total_memory) * 100)
|
||||||
|
: null
|
||||||
|
|
||||||
|
// 计算所有CPU核心的平均频率(统一转换为GHz)
|
||||||
|
const averageCpuFrequency =
|
||||||
|
allSysData?.cpus && allSysData.cpus.length > 0
|
||||||
|
? (() => {
|
||||||
|
// 尝试多个可能的频率字段名
|
||||||
|
const frequencies = allSysData.cpus
|
||||||
|
.map((cpu) => {
|
||||||
|
// 尝试不同的字段名
|
||||||
|
const freq = (cpu as any).frequency ?? (cpu as any).freq ?? (cpu as any).clock_speed
|
||||||
|
return freq
|
||||||
|
})
|
||||||
|
.filter((freq): freq is number => {
|
||||||
|
// 确保是有效的数字且大于0
|
||||||
|
return typeof freq === "number" && !isNaN(freq) && freq > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
if (frequencies.length === 0) {
|
||||||
|
console.log("未找到有效的CPU频率数据,CPU对象:", allSysData.cpus[0])
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const sum = frequencies.reduce((acc, freq) => acc + freq, 0)
|
||||||
|
const avg = sum / frequencies.length
|
||||||
|
|
||||||
|
// 判断单位并统一转换为GHz
|
||||||
|
// 如果值在2-10范围,可能是GHz
|
||||||
|
// 如果值在2000-10000范围,可能是MHz(需要除以1000)
|
||||||
|
// 如果值在百万级别(2000000+),可能是Hz(需要除以1,000,000)
|
||||||
|
let freqInGhz: number
|
||||||
|
if (avg >= 1_000_000) {
|
||||||
|
// Hz单位,转换为GHz
|
||||||
|
freqInGhz = avg / 1_000_000
|
||||||
|
} else if (avg >= 1000) {
|
||||||
|
// MHz单位,转换为GHz
|
||||||
|
freqInGhz = avg / 1000
|
||||||
|
} else {
|
||||||
|
// 已经是GHz单位
|
||||||
|
freqInGhz = avg
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("CPU频率数据:", frequencies, "原始平均值:", avg, "转换为GHz:", freqInGhz)
|
||||||
|
|
||||||
|
return freqInGhz
|
||||||
|
})()
|
||||||
|
: null
|
||||||
|
|
||||||
|
// 如果正在加载,显示 Skeleton 骨架屏
|
||||||
|
if (isLoading || !allSysData) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{/* 系统信息 Skeleton */}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Skeleton className="w-24 h-5 rounded" />
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Skeleton className="w-48 h-8 rounded-full" />
|
||||||
|
<Skeleton className="w-32 h-8 rounded-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CPU 信息 Skeleton */}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Skeleton className="w-20 h-5 rounded" />
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Skeleton className="w-56 h-8 rounded-full" />
|
||||||
|
<Skeleton className="w-32 h-8 rounded-full" />
|
||||||
|
<Skeleton className="h-8 rounded-full w-28" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 内存信息 Skeleton */}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Skeleton className="w-16 h-5 rounded" />
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Skeleton className="h-8 rounded-full w-36" />
|
||||||
|
<Skeleton className="w-40 h-8 rounded-full" />
|
||||||
|
<Skeleton className="h-8 rounded-full w-36" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* GPU 信息 Skeleton */}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Skeleton className="w-16 h-5 rounded" />
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Skeleton className="h-8 rounded-full w-52" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 主板信息 Skeleton */}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Skeleton className="w-16 h-5 rounded" />
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Skeleton className="w-40 h-8 rounded-full" />
|
||||||
|
<Skeleton className="h-8 rounded-full w-36" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{/* 系统信息 */}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="text-sm font-medium text-foreground-600">系统信息</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Chip>
|
||||||
|
<span className="font-medium">系统:</span> {formatSystemInfo()}
|
||||||
|
</Chip>
|
||||||
|
{computerInfo.CsName && (
|
||||||
|
<Chip>
|
||||||
|
<span className="font-medium">主机名:</span> {computerInfo.CsName}
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
{!computerInfo.CsName && allSysData?.hostname && (
|
||||||
|
<Chip>
|
||||||
|
<span className="font-medium">主机名:</span> {allSysData.hostname}
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CPU 信息 */}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="text-sm font-medium text-foreground-600">处理器</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Chip>
|
||||||
|
<span className="font-medium">型号:</span> {allSysData?.cpus?.[0]?.brand || "未知"}
|
||||||
|
</Chip>
|
||||||
|
{averageCpuFrequency !== null && (
|
||||||
|
<Chip>
|
||||||
|
<span className="font-medium">频率:</span> {averageCpuFrequency.toFixed(2)} GHz
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
<Chip>
|
||||||
|
<span className="font-medium">核心数:</span> {allSysData?.cpu_count || "未知"}
|
||||||
|
</Chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 内存信息 */}
|
||||||
|
{allSysData?.total_memory && (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="text-sm font-medium text-foreground-600">内存</div>
|
||||||
|
{/* 第一行:总容量和已用内存 */}
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Chip>
|
||||||
|
<span className="font-medium">总容量:</span> {formatBytes(allSysData.total_memory)}
|
||||||
|
</Chip>
|
||||||
|
{allSysData.used_memory !== undefined && (
|
||||||
|
<Chip>
|
||||||
|
<span className="font-medium">已用:</span> {formatBytes(allSysData.used_memory)}
|
||||||
|
{memoryUsagePercent !== null && (
|
||||||
|
<span className="ml-1">({memoryUsagePercent}%)</span>
|
||||||
|
)}
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* 每个内存条的详细信息 */}
|
||||||
|
{memoryInfo.length > 0 && (
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
{memoryInfo.map((mem, index) => (
|
||||||
|
<div key={index} className="flex flex-wrap gap-2">
|
||||||
|
{mem.capacity && (
|
||||||
|
<Chip>
|
||||||
|
<span className="font-medium">容量:</span> {formatBytes(mem.capacity)}
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
{mem.manufacturer && (
|
||||||
|
<Chip>
|
||||||
|
<span className="font-medium">制造商:</span> {mem.manufacturer}
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
{mem.speed && (
|
||||||
|
<Chip>
|
||||||
|
<span className="font-medium">频率:</span> {mem.speed} MHz
|
||||||
|
{mem.default_speed && (
|
||||||
|
<span className="ml-1">({mem.default_speed} MHz)</span>
|
||||||
|
)}
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* GPU 信息 */}
|
||||||
|
{gpuInfo ? (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="text-sm font-medium text-foreground-600">显卡</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Chip>
|
||||||
|
<span className="font-medium">型号:</span> {gpuInfo.model}
|
||||||
|
</Chip>
|
||||||
|
<Chip>
|
||||||
|
<span className="font-medium">总显存:</span> {formatBytes(gpuInfo.total_vram)}
|
||||||
|
</Chip>
|
||||||
|
<Chip>
|
||||||
|
<span className="font-medium">已用显存:</span> {formatBytes(gpuInfo.used_vram)}
|
||||||
|
</Chip>
|
||||||
|
<Chip>
|
||||||
|
<span className="font-medium">温度:</span> {gpuInfo.temperature.toFixed(2)}°C
|
||||||
|
</Chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
allSysData?.components &&
|
||||||
|
allSysData.components.length > 0 &&
|
||||||
|
(() => {
|
||||||
|
const gpuComponents = allSysData.components.filter(
|
||||||
|
(comp) =>
|
||||||
|
comp.label?.toLowerCase().includes("gpu") ||
|
||||||
|
comp.label?.toLowerCase().includes("graphics") ||
|
||||||
|
comp.label?.toLowerCase().includes("显卡")
|
||||||
|
)
|
||||||
|
|
||||||
|
if (gpuComponents.length === 0) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="text-sm font-medium text-foreground-600">显卡</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{gpuComponents.map((gpu, index) => (
|
||||||
|
<Chip key={index}>
|
||||||
|
<span className="font-medium">GPU{index > 0 ? ` ${index + 1}` : ""}:</span>{" "}
|
||||||
|
{gpu.label || "未知"}
|
||||||
|
{gpu.temperature !== undefined && (
|
||||||
|
<span className="ml-1">
|
||||||
|
({gpu.temperature}°C{gpu.max !== undefined ? ` / ${gpu.max}°C` : ""})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 主板信息 */}
|
||||||
|
{(motherboardInfo?.model ||
|
||||||
|
motherboardInfo?.manufacturer ||
|
||||||
|
motherboardInfo?.version ||
|
||||||
|
computerInfo.BiosSMBIOSBIOSVersion) && (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="text-sm font-medium text-foreground-600">主板</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{motherboardInfo?.model && (
|
||||||
|
<Chip>
|
||||||
|
<span className="font-medium">型号:</span> {motherboardInfo.model}
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
{motherboardInfo?.manufacturer && (
|
||||||
|
<Chip>
|
||||||
|
<span className="font-medium">品牌:</span> {motherboardInfo.manufacturer}
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{motherboardInfo?.version && (
|
||||||
|
<Chip>
|
||||||
|
<span className="font-medium">版本:</span> {motherboardInfo.version}
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
{computerInfo.BiosSMBIOSBIOSVersion && (
|
||||||
|
<Chip>
|
||||||
|
<span className="font-medium">BIOS版本:</span> {computerInfo.BiosSMBIOSBIOSVersion}
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 显示器信息 */}
|
||||||
|
{monitorInfo.length > 0 && monitorInfo.some((m) => m.refresh_rate) && (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="text-sm font-medium text-foreground-600">显示器</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{monitorInfo.map(
|
||||||
|
(monitor, index) =>
|
||||||
|
monitor.refresh_rate && (
|
||||||
|
<Chip key={index}>
|
||||||
|
<span className="font-medium">
|
||||||
|
刷新率{monitorInfo.length > 1 ? ` ${index + 1}` : ""}:
|
||||||
|
</span>{" "}
|
||||||
|
{monitor.refresh_rate} Hz
|
||||||
|
</Chip>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 电池信息 */}
|
||||||
|
{allSysData?.batteries && allSysData.batteries.length > 0 && (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="text-sm font-medium text-foreground-600">电池</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{allSysData.batteries.map((battery, index) => (
|
||||||
|
<Chip key={index}>
|
||||||
|
<span className="font-medium">电池{index > 0 ? ` ${index + 1}` : ""}:</span>
|
||||||
|
{battery.state && `${battery.state} `}
|
||||||
|
{battery.state_of_charge !== undefined && `${battery.state_of_charge}% `}
|
||||||
|
{battery.energy_full !== undefined && battery.energy !== undefined && (
|
||||||
|
<span>
|
||||||
|
({formatBytes(battery.energy)} / {formatBytes(battery.energy_full)})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import SmartTransfer from "@/components/cstb/SmartTranser"
|
|||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
return (
|
return (
|
||||||
<section className="flex flex-col h-full gap-4">
|
<section className="flex flex-col h-full gap-4 overflow-hidden">
|
||||||
<div className="flex flex-grow w-full gap-4">
|
<div className="flex flex-grow w-full gap-4">
|
||||||
<Notice />
|
<Notice />
|
||||||
<SmartTransfer />
|
<SmartTransfer />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CommonDir />
|
<CommonDir />
|
||||||
|
|||||||
@@ -1,4 +1,66 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "@/components/window/Card"
|
||||||
|
import { MovieBoard } from "@icon-park/react"
|
||||||
|
import { ToolButton } from "@/components/window/ToolButton"
|
||||||
|
import { invoke } from "@tauri-apps/api/core"
|
||||||
|
import path from "path"
|
||||||
|
import { useSteamStore } from "@/store/steam"
|
||||||
|
import { addToast } from "@heroui/react"
|
||||||
|
// import { Command } from "@tauri-apps/plugin-shell"
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <div>Movie</div>
|
const steam = useSteamStore()
|
||||||
|
const testDemo = async (demo_name: string) => {
|
||||||
|
const res = await invoke("analyze_replay", {
|
||||||
|
path: path.resolve(
|
||||||
|
steam.cs2BaseDir(),
|
||||||
|
"game",
|
||||||
|
"csgo",
|
||||||
|
"replays",
|
||||||
|
"test.dem"
|
||||||
|
),
|
||||||
|
})
|
||||||
|
console.log("test.dem", "→", res)
|
||||||
|
// const demo_path = path.resolve(
|
||||||
|
// '"',
|
||||||
|
// steam.cs2BaseDir(),
|
||||||
|
// "game",
|
||||||
|
// "csgo",
|
||||||
|
// "replays",
|
||||||
|
// demo_name,
|
||||||
|
// '"'
|
||||||
|
// )
|
||||||
|
// const command = Command.sidecar("bin/csda", [
|
||||||
|
// "-demo-path",
|
||||||
|
// "D:\\Programs\\Steam\\steamapps\\common\\Counter-Strike Global Offensive\\game\\csgo\\replays",
|
||||||
|
// "-format",
|
||||||
|
// "json",
|
||||||
|
// "-minify",
|
||||||
|
// ])
|
||||||
|
// const output = await command.execute()
|
||||||
|
// console.log("output", output)
|
||||||
|
addToast({ title: "解析成功" })
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<section className="flex flex-col gap-4 overflow-hidden">
|
||||||
|
<Card className="overflow-hidden">
|
||||||
|
<CardHeader>
|
||||||
|
<CardIcon>
|
||||||
|
<MovieBoard /> 录像
|
||||||
|
</CardIcon>
|
||||||
|
<CardTool>
|
||||||
|
<ToolButton
|
||||||
|
onClick={async () => {
|
||||||
|
await testDemo("test.dem")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
读取
|
||||||
|
</ToolButton>
|
||||||
|
</CardTool>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody className="overflow-y-hidden">录像</CardBody>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,114 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
import { useEffect } from "react"
|
||||||
import { useAppStore } from "@/store/app"
|
import { useAppStore } from "@/store/app"
|
||||||
import { Switch } from "@heroui/react"
|
import { Switch, Chip } from "@heroui/react"
|
||||||
|
import { UpdateChecker } from "@/components/cstb/UpdateChecker"
|
||||||
|
import { getVersion } from "@tauri-apps/api/app"
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const app = useAppStore()
|
const app = useAppStore()
|
||||||
|
|
||||||
|
// 初始化版本号(如果还没有设置)
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== "undefined" && (!app.state.version || app.state.version === "0.0.1")) {
|
||||||
|
void getVersion().then((version) => {
|
||||||
|
app.setVersion(version)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 从环境变量或配置中获取更新服务器地址
|
||||||
|
const customEndpoint = process.env.NEXT_PUBLIC_UPDATE_ENDPOINT || ""
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-start gap-3 pt-2 pb-1">
|
<section className="flex flex-col gap-4 overflow-hidden">
|
||||||
<p>版本号:{app.state.version}</p>
|
<div className="flex flex-col items-start gap-4 pt-2 pb-1">
|
||||||
<p>是否有更新:{app.state.hasUpdate ? "有" : "无"}</p>
|
<div className="space-y-2">
|
||||||
<p>是否使用镜像源:{app.state.useMirror ? "是" : "否"}</p>
|
<div className="flex items-center gap-2">
|
||||||
<Switch
|
<p className="text-sm">版本号:{app.state.version}</p>
|
||||||
isSelected={app.state.autoStart}
|
{app.state.hasUpdate && app.state.latestVersion && (
|
||||||
size="sm"
|
<Chip size="sm" color="success" variant="flat">
|
||||||
onChange={(e) => app.setAutoStart(e.target.checked)}
|
{app.state.latestVersion}
|
||||||
>
|
</Chip>
|
||||||
开机自启动 {app.state.autoStart ? "开" : "关"}
|
)}
|
||||||
</Switch>
|
</div>
|
||||||
</div>
|
{/* <p className="text-sm">是否有更新:{app.state.hasUpdate ? "有" : "无"}</p> */}
|
||||||
|
{/* <p className="text-sm">是否使用镜像源:{app.state.useMirror ? "是" : "否"}</p> */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full pt-4 border-t border-zinc-200 dark:border-zinc-800">
|
||||||
|
<h3 className="mb-3 text-sm font-semibold">更新检查</h3>
|
||||||
|
<div className="mb-3 space-y-3">
|
||||||
|
{/* <Switch
|
||||||
|
isSelected={app.state.useMirror}
|
||||||
|
size="sm"
|
||||||
|
onChange={(e) => app.setUseMirror(e.target.checked)}
|
||||||
|
>
|
||||||
|
使用镜像源
|
||||||
|
</Switch> */}
|
||||||
|
{/* <p className="text-xs text-zinc-500">
|
||||||
|
{app.state.useMirror
|
||||||
|
? "使用自建更新服务检查更新"
|
||||||
|
: "使用 GitHub Release 检查更新"}
|
||||||
|
</p> */}
|
||||||
|
<Switch
|
||||||
|
isSelected={app.state.includePrerelease}
|
||||||
|
size="sm"
|
||||||
|
onChange={(e) => app.setIncludePrerelease(e.target.checked)}
|
||||||
|
>
|
||||||
|
包含测试版
|
||||||
|
</Switch>
|
||||||
|
<p className="text-xs text-zinc-500">
|
||||||
|
{app.state.includePrerelease
|
||||||
|
? "检查更新时会包含预发布版本(beta、alpha等)"
|
||||||
|
: "仅检查正式版本"}
|
||||||
|
</p>
|
||||||
|
<Switch
|
||||||
|
isSelected={app.state.useCdn}
|
||||||
|
size="sm"
|
||||||
|
onChange={(e) => app.setUseCdn(e.target.checked)}
|
||||||
|
>
|
||||||
|
使用 CDN 加速下载
|
||||||
|
</Switch>
|
||||||
|
<p className="text-xs text-zinc-500">
|
||||||
|
{app.state.useCdn
|
||||||
|
? "加速下载,避免 GitHub 无法正常访问"
|
||||||
|
: "直接从 GitHub 下载更新文件"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<UpdateChecker
|
||||||
|
customEndpoint={customEndpoint || undefined}
|
||||||
|
includePrerelease={app.state.includePrerelease}
|
||||||
|
useCdn={app.state.useCdn}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-full pt-4 space-y-3 border-t border-zinc-200 dark:border-zinc-800">
|
||||||
|
<h3 className="mb-3 text-sm font-semibold">启动设置</h3>
|
||||||
|
<Switch
|
||||||
|
isSelected={app.state.autoStart}
|
||||||
|
size="sm"
|
||||||
|
onChange={(e) => app.setAutoStart(e.target.checked)}
|
||||||
|
>
|
||||||
|
开机自启动
|
||||||
|
</Switch>
|
||||||
|
<Switch
|
||||||
|
isSelected={app.state.startHidden}
|
||||||
|
size="sm"
|
||||||
|
onChange={(e) => app.setStartHidden(e.target.checked)}
|
||||||
|
>
|
||||||
|
静默启动
|
||||||
|
</Switch>
|
||||||
|
<Switch
|
||||||
|
isSelected={app.state.hiddenOnClose}
|
||||||
|
size="sm"
|
||||||
|
onChange={(e) => app.setHiddenOnClose(e.target.checked)}
|
||||||
|
>
|
||||||
|
关闭时最小化到托盘
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ export default function Page() {
|
|||||||
const steam = useSteamStore()
|
const steam = useSteamStore()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-start gap-3 pt-2 pb-1">
|
<section className="flex flex-col gap-4 overflow-hidden">
|
||||||
<p>Steam路径:{steam.state.steamDir}</p>
|
<div className="flex flex-col items-start gap-3 pt-2 pb-1">
|
||||||
<p>游戏路径:{steam.state.cs2Dir}</p>
|
<p>Steam路径:{steam.state.steamDir}</p>
|
||||||
<p>Steam路径有效:{steam.state.steamDirValid ? "是" : "否"}</p>
|
<p>游戏路径:{steam.state.cs2Dir}</p>
|
||||||
<p>游戏路径有效:{steam.state.cs2DirValid ? "是" : "否"}</p>
|
<p>Steam路径有效:{steam.state.steamDirValid ? "是" : "否"}</p>
|
||||||
<p>Steam账号:{steam.currentUser()?.account_name || " "}</p>
|
<p>游戏路径有效:{steam.state.cs2DirValid ? "是" : "否"}</p>
|
||||||
</div>
|
<p>Steam账号:{steam.currentUser()?.account_name || " "}</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <>Replay</>
|
return (
|
||||||
|
<section className="flex flex-col gap-4 overflow-hidden">
|
||||||
|
<>Replay</>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import VideoSetting from "@/components/cstb/VideoSetting"
|
import VideoSetting from "@/components/cstb/VideoSetting"
|
||||||
|
import { FpsTest } from "@/components/cstb/FpsTest"
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<section className="flex flex-col h-full gap-4">
|
<section className="flex flex-col h-full gap-4 overflow-hidden">
|
||||||
<VideoSetting />
|
<div className="flex flex-col h-full gap-4 overflow-y-auto rounded-lg hide-scrollbar">
|
||||||
|
<VideoSetting />
|
||||||
|
<FpsTest />
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default function PreferenceLayout({
|
|||||||
// const pathname = usePathname()
|
// const pathname = usePathname()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full gap-3">
|
<div className="flex w-full h-full gap-3 overflow-hidden">
|
||||||
{/* <Card className="flex-grow max-w-ful">
|
{/* <Card className="flex-grow max-w-ful">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardIcon
|
<CardIcon
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <div>Users</div>
|
return (
|
||||||
|
<section className="flex flex-col gap-4 overflow-hidden">
|
||||||
|
<div>Users</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
@tailwind base;
|
@import "tailwindcss";
|
||||||
@tailwind components;
|
@config "../../tailwind.config.js";
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
@@ -21,3 +20,12 @@ a {
|
|||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 隐藏滚动条 */
|
||||||
|
.hide-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.hide-scrollbar {
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
@@ -1,21 +1,99 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useSteamStore } from "@/store/steam"
|
import { init } from "@/store"
|
||||||
|
import { steamStore, useSteamStore } from "@/store/steam"
|
||||||
|
import { toolStore, useToolStore } from "@/store/tool"
|
||||||
|
import { addToast } from "@heroui/react"
|
||||||
|
import { invoke } from "@tauri-apps/api/core"
|
||||||
|
import { listen } from "@tauri-apps/api/event"
|
||||||
|
import { useDebounce, useThrottleFn } from "ahooks"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
import "./globals.css"
|
import "./globals.css"
|
||||||
import Providers from "./providers"
|
import Providers from "./providers"
|
||||||
import { init } from "@/store"
|
import { PowerPlans } from "@/components/cstb/PowerPlan"
|
||||||
import { useDebounce } from "ahooks"
|
|
||||||
|
|
||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
const steam = useSteamStore()
|
||||||
|
const tool = useToolStore()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void init()
|
void init()
|
||||||
})
|
|
||||||
|
void listen<string>("tray://launch_game", async (event) => {
|
||||||
|
// 验证路径
|
||||||
|
if (!steamStore.state.steamDir || !steamStore.state.steamDirValid) {
|
||||||
|
addToast({
|
||||||
|
title: "Steam 路径无效,请先配置路径",
|
||||||
|
color: "warning"
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await invoke("launch_game", {
|
||||||
|
steamPath: `${steamStore.state.steamDir}\\steam.exe`,
|
||||||
|
launchOption: toolStore.state.launchOptions[toolStore.state.launchIndex].option || "",
|
||||||
|
server: event.payload || "worldwide",
|
||||||
|
})
|
||||||
|
addToast({
|
||||||
|
title: `启动${event.payload === "worldwide" ? "国际服" : "国服"}成功`,
|
||||||
|
color: "success"
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("启动游戏失败:", error)
|
||||||
|
addToast({
|
||||||
|
title: `启动失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
color: "danger"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
void listen("tray://kill_steam", async () => {
|
||||||
|
await invoke("kill_steam")
|
||||||
|
addToast({ title: "已关闭Steam" })
|
||||||
|
})
|
||||||
|
|
||||||
|
void listen("tray://kill_game", async () => {
|
||||||
|
await invoke("kill_game")
|
||||||
|
addToast({ title: "已关闭CS2" })
|
||||||
|
})
|
||||||
|
|
||||||
|
void listen<number>("tray://set_powerplan", async (event) => {
|
||||||
|
if (typeof event.payload === "number" && event.payload <= 0 && event.payload > 4) return
|
||||||
|
await invoke("set_powerplan", { plan: event.payload })
|
||||||
|
const current = await invoke<number>("get_powerplan")
|
||||||
|
tool.setPowerPlan(current)
|
||||||
|
|
||||||
|
addToast({ title: `电源计划已切换 → ${PowerPlans[current].title}` })
|
||||||
|
})
|
||||||
|
|
||||||
|
void listen<number>("tray://set_launch_index", async (event) => {
|
||||||
|
const index = event.payload
|
||||||
|
if (typeof index === "number" && index >= 0 && index < toolStore.state.launchOptions.length) {
|
||||||
|
tool.setLaunchIndex(index)
|
||||||
|
const optionName = toolStore.state.launchOptions[index].name || `启动项 ${index + 1}`
|
||||||
|
addToast({ title: `启动项已切换 → ${optionName}` })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
// 检测steam路径和游戏路径是否有效
|
// 检测steam路径和游戏路径是否有效
|
||||||
const steam = useSteamStore()
|
const debounceSteamDir = useDebounce(steam.state.steamDir, {
|
||||||
const debounceSteamDir = useDebounce(steam.state.steamDir, {wait: 500, leading: true, trailing: true, maxWait: 2500})
|
wait: 500,
|
||||||
const debounceCs2Dir = useDebounce(steam.state.cs2Dir, {wait: 500, leading: true, trailing: true, maxWait: 2500})
|
leading: true,
|
||||||
const debounceSteamDirValid = useDebounce(steam.state.steamDirValid, {wait: 500, leading: true, trailing: true, maxWait: 2500})
|
trailing: true,
|
||||||
|
maxWait: 2500,
|
||||||
|
})
|
||||||
|
const debounceCs2Dir = useDebounce(steam.state.cs2Dir, {
|
||||||
|
wait: 500,
|
||||||
|
leading: true,
|
||||||
|
trailing: true,
|
||||||
|
maxWait: 2500,
|
||||||
|
})
|
||||||
|
const debounceSteamDirValid = useDebounce(steam.state.steamDirValid, {
|
||||||
|
wait: 500,
|
||||||
|
leading: true,
|
||||||
|
trailing: true,
|
||||||
|
maxWait: 2500,
|
||||||
|
})
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void steam.checkSteamDirValid()
|
void steam.checkSteamDirValid()
|
||||||
}, [debounceSteamDir])
|
}, [debounceSteamDir])
|
||||||
@@ -23,10 +101,40 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
void steam.checkCs2DirValid()
|
void steam.checkCs2DirValid()
|
||||||
}, [debounceCs2Dir])
|
}, [debounceCs2Dir])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (debounceSteamDirValid) {
|
if (debounceSteamDirValid && steam.state.steamDir) {
|
||||||
|
// 安全地获取用户列表(内部已有错误处理)
|
||||||
void steam.getUsers()
|
void steam.getUsers()
|
||||||
|
// 启动文件监听,添加错误处理
|
||||||
|
void invoke("start_watch_loginusers", { steamDir: steam.state.steamDir }).catch((error) => {
|
||||||
|
console.error("启动文件监听失败:", error)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, [debounceSteamDirValid])
|
}, [debounceSteamDirValid, steam.state.steamDir])
|
||||||
|
|
||||||
|
// 节流获取用户列表函数,3秒间隔,trailing模式
|
||||||
|
const { run: throttledGetUsers } = useThrottleFn(
|
||||||
|
async () => {
|
||||||
|
if (steam.state.steamDirValid) {
|
||||||
|
await steam.getUsers()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
wait: 3000,
|
||||||
|
leading: false,
|
||||||
|
trailing: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 监听 loginusers.vdf 文件变化事件
|
||||||
|
useEffect(() => {
|
||||||
|
const unlisten = listen("steam://loginusers_changed", async () => {
|
||||||
|
// 文件变化时使用节流函数重新获取用户列表
|
||||||
|
throttledGetUsers()
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
void unlisten.then((fn) => fn())
|
||||||
|
}
|
||||||
|
}, [steam.state.steamDirValid, throttledGetUsers])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|||||||
@@ -17,29 +17,39 @@ export default function Page() {
|
|||||||
className="flex flex-col items-center justify-center w-full h-screen gap-6"
|
className="flex flex-col items-center justify-center w-full h-screen gap-6"
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
>
|
>
|
||||||
<h1 className="text-4xl font-bold tracking-wide">CS工具箱</h1>
|
<h1 className="text-4xl font-bold tracking-wide">CS 工具箱</h1>
|
||||||
<p>准备环节</p>
|
<p className="text-sm text-zinc-500">配置页面</p>
|
||||||
|
|
||||||
<div className="flex flex-col w-full gap-2 p-5 border rounded-lg bg-white/40">
|
<div className="flex flex-col w-full max-w-2xl gap-4 p-5 border rounded-lg bg-white/40 dark:bg-zinc-800/40">
|
||||||
<p>Steam所在文件夹</p>
|
<div className="space-y-2">
|
||||||
<input
|
<p className="text-sm font-semibold">Steam 安装目录</p>
|
||||||
className="px-2 py-1 mb-2 rounded-lg"
|
<input
|
||||||
value={steamDir}
|
className="w-full px-3 py-2 rounded-lg bg-white dark:bg-zinc-700 border border-zinc-300 dark:border-zinc-600"
|
||||||
onChange={(e) => {
|
placeholder="请输入 Steam 安装路径"
|
||||||
setSteamDir(e.target.value)
|
value={steamDir}
|
||||||
steam.setDir(e.target.value)
|
onChange={(e) => {
|
||||||
}}
|
setSteamDir(e.target.value)
|
||||||
/>
|
steam.setDir(e.target.value)
|
||||||
<p>CS2所在文件夹</p>
|
}}
|
||||||
<input
|
/>
|
||||||
className="px-2 py-1 mb-2 rounded-lg"
|
</div>
|
||||||
value={cs2Dir}
|
<div className="space-y-2">
|
||||||
onChange={(e) => {
|
<p className="text-sm font-semibold">CS2 安装目录</p>
|
||||||
setCs2Dir(e.target.value)
|
<input
|
||||||
steam.setCsDir(e.target.value)
|
className="w-full px-3 py-2 rounded-lg bg-white dark:bg-zinc-700 border border-zinc-300 dark:border-zinc-600"
|
||||||
}}
|
placeholder="请输入 CS2 安装路径"
|
||||||
/>
|
value={cs2Dir}
|
||||||
<p>当前用户64位SteamID:{steam.currentUser()?.steam_id64}</p>
|
onChange={(e) => {
|
||||||
|
setCs2Dir(e.target.value)
|
||||||
|
steam.setCsDir(e.target.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{steam.currentUser()?.steam_id64 && (
|
||||||
|
<p className="text-xs text-zinc-500">
|
||||||
|
当前用户 64 位 Steam ID:{steam.currentUser()?.steam_id64}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ToastProvider } from "@heroui/toast"
|
|||||||
import { platform } from "@tauri-apps/plugin-os"
|
import { platform } from "@tauri-apps/plugin-os"
|
||||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
|
import { AuthProvider } from "@/components/auth/AuthProvider"
|
||||||
|
|
||||||
export default function Providers({ children }: { children: React.ReactNode }) {
|
export default function Providers({ children }: { children: React.ReactNode }) {
|
||||||
const [os, setOs] = useState("windows")
|
const [os, setOs] = useState("windows")
|
||||||
@@ -13,11 +14,11 @@ export default function Providers({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<HeroUIProvider
|
<HeroUIProvider
|
||||||
className={cn("h-full bg-zinc-200/90 dark:bg-zinc-900", os === "macos" && "rounded-lg")}
|
className={cn("h-full bg-zinc-100/95 dark:bg-zinc-900/95", os === "macos" && "rounded-lg")}
|
||||||
>
|
>
|
||||||
<NextThemesProvider attribute="class" defaultTheme="light">
|
<NextThemesProvider attribute="class" defaultTheme="light">
|
||||||
<ToastProvider toastOffset={10} placement="top-center" toastProps={{ timeout: 3000 }} />
|
<ToastProvider toastOffset={10} placement="top-center" toastProps={{ timeout: 3000 }} />
|
||||||
{children}
|
<AuthProvider>{children}</AuthProvider>
|
||||||
</NextThemesProvider>
|
</NextThemesProvider>
|
||||||
</HeroUIProvider>
|
</HeroUIProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useCallback, useState } from "react"
|
|||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [buttonDesc, setButtonDesc] = useState<string>(
|
const [buttonDesc, setButtonDesc] = useState<string>(
|
||||||
"Waiting to be clicked. This calls 'on_button_clicked' from Rust.",
|
"等待点击。这将调用 Rust 中的 'on_button_clicked' 命令。",
|
||||||
)
|
)
|
||||||
const onButtonClick = () => {
|
const onButtonClick = () => {
|
||||||
invoke<string>("on_button_clicked")
|
invoke<string>("on_button_clicked")
|
||||||
@@ -14,7 +14,7 @@ export default function Page() {
|
|||||||
setButtonDesc(value)
|
setButtonDesc(value)
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setButtonDesc("Failed to invoke Rust command 'on_button_clicked'")
|
setButtonDesc("调用 Rust 命令 'on_button_clicked' 失败")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ export default function Page() {
|
|||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<main className="flex flex-col items-center justify-center flex-1 py-8">
|
<main className="flex flex-col items-center justify-center flex-1 py-8">
|
||||||
<h1 className="m-0 text-6xl text-center">
|
<h1 className="m-0 text-6xl text-center">
|
||||||
Welcome to{" "}
|
欢迎使用{" "}
|
||||||
<a
|
<a
|
||||||
href="https://nextjs.org"
|
href="https://nextjs.org"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -39,7 +39,7 @@ export default function Page() {
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="my-12 text-2xl leading-9 text-center">
|
<p className="my-12 text-2xl leading-9 text-center">
|
||||||
Get started by editing{" "}
|
开始编辑{" "}
|
||||||
<code className="p-2 font-mono text-xl bg-gray-200 rounded-md">
|
<code className="p-2 font-mono text-xl bg-gray-200 rounded-md">
|
||||||
src/pages/index.tsx
|
src/pages/index.tsx
|
||||||
</code>
|
</code>
|
||||||
@@ -48,7 +48,7 @@ export default function Page() {
|
|||||||
<div className="flex flex-wrap items-center justify-center max-w-3xl">
|
<div className="flex flex-wrap items-center justify-center max-w-3xl">
|
||||||
<CardButton
|
<CardButton
|
||||||
onClick={onButtonClick}
|
onClick={onButtonClick}
|
||||||
title="Tauri Invoke"
|
title="Tauri 调用"
|
||||||
description={buttonDesc}
|
description={buttonDesc}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
142
src/components/auth/AuthButton.tsx
Normal file
142
src/components/auth/AuthButton.tsx
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Button, Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Avatar, Spinner } from "@heroui/react"
|
||||||
|
import { User, Logout, Login, AddUser } from "@icon-park/react"
|
||||||
|
import { useAuthStore } from "@/store/auth"
|
||||||
|
import { openLoginPage, openSignupPage } from "@/utils/auth"
|
||||||
|
import { useDisclosure } from "@heroui/react"
|
||||||
|
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/react"
|
||||||
|
|
||||||
|
export function AuthButton() {
|
||||||
|
const { state, signOut } = useAuthStore()
|
||||||
|
const { isOpen, onOpen, onOpenChange } = useDisclosure()
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
await openLoginPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSignup = async () => {
|
||||||
|
await openSignupPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSignOut = async () => {
|
||||||
|
await signOut()
|
||||||
|
onOpenChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.isLoading) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
className="px-2 py-0 rounded transition duration-150 hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95 cursor-pointer"
|
||||||
|
>
|
||||||
|
<Spinner size="sm" />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.isAuthenticated && state.user) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dropdown placement="bottom-end">
|
||||||
|
<DropdownTrigger>
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
className="px-2 py-0 rounded transition duration-150 hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95 cursor-pointer [&>*]:cursor-pointer"
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
src={state.user.user_metadata?.avatar_url}
|
||||||
|
name={state.user.email || state.user.id}
|
||||||
|
size="sm"
|
||||||
|
className="w-6 h-6 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</DropdownTrigger>
|
||||||
|
<DropdownMenu aria-label="用户菜单">
|
||||||
|
<DropdownItem
|
||||||
|
key="profile"
|
||||||
|
startContent={<User size={16} />}
|
||||||
|
textValue="用户信息"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-medium">{state.user.email}</span>
|
||||||
|
{state.user.user_metadata?.name && (
|
||||||
|
<span className="text-xs text-zinc-500">{state.user.user_metadata.name}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DropdownItem>
|
||||||
|
<DropdownItem
|
||||||
|
key="logout"
|
||||||
|
startContent={<Logout size={16} />}
|
||||||
|
color="danger"
|
||||||
|
onPress={onOpen}
|
||||||
|
>
|
||||||
|
退出登录
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<ModalContent>
|
||||||
|
{(onClose) => (
|
||||||
|
<>
|
||||||
|
<ModalHeader className="flex flex-col gap-1">退出登录</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<p>确认要退出登录吗?</p>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button color="default" variant="light" onPress={onClose}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="danger"
|
||||||
|
onPress={() => {
|
||||||
|
handleSignOut()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
确认退出
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown placement="bottom-end">
|
||||||
|
<DropdownTrigger>
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
className="px-2 py-0 rounded transition duration-150 hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95 cursor-pointer [&>*]:cursor-pointer"
|
||||||
|
>
|
||||||
|
<User size={16} className="cursor-pointer" />
|
||||||
|
</Button>
|
||||||
|
</DropdownTrigger>
|
||||||
|
<DropdownMenu aria-label="登录菜单">
|
||||||
|
<DropdownItem
|
||||||
|
key="login"
|
||||||
|
startContent={<Login size={16} />}
|
||||||
|
onPress={handleLogin}
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</DropdownItem>
|
||||||
|
<DropdownItem
|
||||||
|
key="signup"
|
||||||
|
startContent={<AddUser size={16} />}
|
||||||
|
onPress={handleSignup}
|
||||||
|
>
|
||||||
|
注册
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
76
src/components/auth/AuthProvider.tsx
Normal file
76
src/components/auth/AuthProvider.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { onOpenUrl } from "@tauri-apps/plugin-deep-link"
|
||||||
|
import { useEffect } from "react"
|
||||||
|
import { useAuthStore } from "@/store/auth"
|
||||||
|
import { handleAuthCallback } from "@/utils/auth"
|
||||||
|
import { createClient } from "@/utils/supabase/client"
|
||||||
|
import { addToast } from "@heroui/react"
|
||||||
|
|
||||||
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const { checkSession, setSession, setLoading } = useAuthStore()
|
||||||
|
|
||||||
|
// 初始化时检查现有会话
|
||||||
|
useEffect(() => {
|
||||||
|
void checkSession()
|
||||||
|
}, [checkSession])
|
||||||
|
|
||||||
|
// 监听 deep-link 认证回调
|
||||||
|
useEffect(() => {
|
||||||
|
const unlisten = onOpenUrl(async (urls) => {
|
||||||
|
if (urls.length === 0) return
|
||||||
|
|
||||||
|
const url = urls[0]
|
||||||
|
if (!url.startsWith("cstb://auth")) return
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const session = await handleAuthCallback(url)
|
||||||
|
if (session) {
|
||||||
|
setSession(session)
|
||||||
|
addToast({
|
||||||
|
title: "登录成功",
|
||||||
|
description: `欢迎回来,${session.user.email || session.user.id}`,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
addToast({
|
||||||
|
title: "登录失败",
|
||||||
|
description: "无法验证登录信息,请重试",
|
||||||
|
|
||||||
|
severity: "danger",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Auth callback error:", error)
|
||||||
|
addToast({
|
||||||
|
title: "登录失败",
|
||||||
|
description: error instanceof Error ? error.message : "未知错误",
|
||||||
|
severity: "danger",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
void unlisten.then((fn) => fn())
|
||||||
|
}
|
||||||
|
}, [setSession, setLoading])
|
||||||
|
|
||||||
|
// 监听 Supabase 认证状态变化
|
||||||
|
useEffect(() => {
|
||||||
|
const supabase = createClient()
|
||||||
|
const {
|
||||||
|
data: { subscription },
|
||||||
|
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||||
|
setSession(session)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
subscription.unsubscribe()
|
||||||
|
}
|
||||||
|
}, [setSession])
|
||||||
|
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ const RoundedButton = ({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center justify-center px-3 py-1 transition rounded-full select-none min-w-fit active:scale-95 hover:bg-black/10 text-zinc-700 dark:text-zinc-100 bg-black/5 dark:bg-white/5"
|
className="flex items-center justify-center px-3 py-1 transition rounded-full cursor-pointer select-none min-w-fit active:scale-95 hover:bg-black/10 text-zinc-700 dark:text-zinc-100 bg-black/5 dark:bg-white/5"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -51,10 +51,10 @@ const CommonDir = () => {
|
|||||||
await invoke("open_path", {
|
await invoke("open_path", {
|
||||||
path: steam.cs2BaseDir(),
|
path: steam.cs2BaseDir(),
|
||||||
})
|
})
|
||||||
addToast({ title: "CS2游戏目录" })
|
addToast({ title: "CS2目录" })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
CS2游戏目录
|
CS2目录
|
||||||
</RoundedButton>
|
</RoundedButton>
|
||||||
<RoundedButton
|
<RoundedButton
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
|||||||
@@ -20,29 +20,61 @@ const FastLaunch = () => {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
size="md"
|
size="md"
|
||||||
onPress={() => {
|
onPress={async () => {
|
||||||
void invoke("launch_game", {
|
// 验证路径
|
||||||
steamPath: `${steam.state.steamDir}/steam.exe`,
|
if (!steam.state.steamDir || !steam.state.steamDirValid) {
|
||||||
launchOption: tool.state.launchOptions[tool.state.launchIndex].option || "",
|
addToast({
|
||||||
server: "perfectworld",
|
title: "Steam 路径无效,请先配置路径",
|
||||||
})
|
color: "warning"
|
||||||
addToast({ title: "启动国服成功" })
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await invoke("launch_game", {
|
||||||
|
steamPath: `${steam.state.steamDir}/steam.exe`,
|
||||||
|
launchOption: tool.state.launchOptions[tool.state.launchIndex].option || "",
|
||||||
|
server: "perfectworld",
|
||||||
|
})
|
||||||
|
addToast({ title: "启动国服成功", color: "success" })
|
||||||
|
} catch (error) {
|
||||||
|
console.error("启动游戏失败:", error)
|
||||||
|
addToast({
|
||||||
|
title: `启动失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
color: "danger"
|
||||||
|
})
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
className="px-5 font-medium py-1.5 transition bg-red-200 dark:bg-red-900/60 rounded-full select-none"
|
className="px-4 py-1 font-medium transition bg-red-200 rounded-full select-none dark:bg-red-900/60"
|
||||||
>
|
>
|
||||||
启动国服
|
启动国服
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="md"
|
size="md"
|
||||||
onPress={() => {
|
onPress={async () => {
|
||||||
void invoke("launch_game", {
|
// 验证路径
|
||||||
steamPath: `${steam.state.steamDir}/steam.exe`,
|
if (!steam.state.steamDir || !steam.state.steamDirValid) {
|
||||||
launchOption: tool.state.launchOptions[tool.state.launchIndex].option || "",
|
addToast({
|
||||||
server: "worldwide",
|
title: "Steam 路径无效,请先配置路径",
|
||||||
})
|
color: "warning"
|
||||||
addToast({ title: "启动国际服成功" })
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await invoke("launch_game", {
|
||||||
|
steamPath: `${steam.state.steamDir}/steam.exe`,
|
||||||
|
launchOption: tool.state.launchOptions[tool.state.launchIndex].option || "",
|
||||||
|
server: "worldwide",
|
||||||
|
})
|
||||||
|
addToast({ title: "启动国际服成功", color: "success" })
|
||||||
|
} catch (error) {
|
||||||
|
console.error("启动游戏失败:", error)
|
||||||
|
addToast({
|
||||||
|
title: `启动失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
color: "danger"
|
||||||
|
})
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
className="px-5 font-medium py-1.5 transition bg-orange-200 dark:bg-orange-900/60 rounded-full select-none"
|
className="px-4 py-1 font-medium transition bg-orange-200 rounded-full select-none dark:bg-orange-900/60"
|
||||||
>
|
>
|
||||||
启动国际服
|
启动国际服
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const ForceQuit = () => {
|
|||||||
await invoke("kill_steam")
|
await invoke("kill_steam")
|
||||||
addToast({ title: "已关闭Steam" })
|
addToast({ title: "已关闭Steam" })
|
||||||
}}
|
}}
|
||||||
className="px-5 font-medium py-1.5 transition bg-blue-200 dark:bg-blue-900/60 rounded-full select-none"
|
className="px-4 py-1 font-medium transition bg-blue-200 rounded-full select-none dark:bg-blue-900/60"
|
||||||
>
|
>
|
||||||
关闭Steam
|
关闭Steam
|
||||||
</Button>
|
</Button>
|
||||||
@@ -29,7 +29,7 @@ const ForceQuit = () => {
|
|||||||
await invoke("kill_game")
|
await invoke("kill_game")
|
||||||
addToast({ title: "已关闭CS2" })
|
addToast({ title: "已关闭CS2" })
|
||||||
}}
|
}}
|
||||||
className="px-5 font-medium py-1.5 transition bg-orange-200 dark:bg-orange-900/60 rounded-full select-none"
|
className="px-4 py-1 font-medium transition bg-orange-200 rounded-full select-none dark:bg-orange-900/60"
|
||||||
>
|
>
|
||||||
关闭CS2
|
关闭CS2
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
1261
src/components/cstb/FpsTest.tsx
Normal file
1261
src/components/cstb/FpsTest.tsx
Normal file
File diff suppressed because it is too large
Load Diff
31
src/components/cstb/FpsTest/components/BatchTestProgress.tsx
Normal file
31
src/components/cstb/FpsTest/components/BatchTestProgress.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { CircularProgress } from "@heroui/react"
|
||||||
|
import type { BatchTestProgress as BatchTestProgressType } from "../types"
|
||||||
|
|
||||||
|
interface BatchTestProgressProps {
|
||||||
|
progress: BatchTestProgressType | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BatchTestProgress({ progress }: BatchTestProgressProps) {
|
||||||
|
if (!progress) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-1.5 items-center justify-center">
|
||||||
|
<div className="relative">
|
||||||
|
<CircularProgress
|
||||||
|
aria-label="批量测试进度"
|
||||||
|
value={(progress.current / progress.total) * 100}
|
||||||
|
color="primary"
|
||||||
|
size="sm"
|
||||||
|
showValueLabel={false}
|
||||||
|
classNames={{
|
||||||
|
svg: " ",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center text-xs font-medium">
|
||||||
|
{progress.current}/{progress.total}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
27
src/components/cstb/FpsTest/components/NoteCell.tsx
Normal file
27
src/components/cstb/FpsTest/components/NoteCell.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Button } from "@heroui/react"
|
||||||
|
import { Edit } from "@icon-park/react"
|
||||||
|
|
||||||
|
interface NoteCellProps {
|
||||||
|
note: string
|
||||||
|
onEdit: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NoteCell({ note, onEdit }: NoteCellProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center min-w-0 gap-1">
|
||||||
|
<span className="flex-1 min-w-0 truncate select-text">
|
||||||
|
{note || "无备注"}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
isIconOnly
|
||||||
|
variant="light"
|
||||||
|
onPress={onEdit}
|
||||||
|
className="h-5 min-w-5 shrink-0"
|
||||||
|
>
|
||||||
|
<Edit size={12} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
193
src/components/cstb/FpsTest/components/ResolutionConfig.tsx
Normal file
193
src/components/cstb/FpsTest/components/ResolutionConfig.tsx
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import { Button, Input, Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Chip } from "@heroui/react"
|
||||||
|
import { List } from "@icon-park/react"
|
||||||
|
import { PRESET_RESOLUTIONS } from "../constants"
|
||||||
|
import type { Resolution } from "../types"
|
||||||
|
import type { useFpsTestStore } from "@/store/fps_test"
|
||||||
|
|
||||||
|
interface ResolutionConfigProps {
|
||||||
|
resolutionWidth: string
|
||||||
|
resolutionHeight: string
|
||||||
|
isResolutionEnabled: boolean
|
||||||
|
isResolutionGroupEnabled: boolean
|
||||||
|
isFullscreen: boolean
|
||||||
|
resolutionGroup: Resolution[]
|
||||||
|
isMonitoring: boolean
|
||||||
|
fpsTest: ReturnType<typeof useFpsTestStore>
|
||||||
|
onPresetResolution: (preset: Resolution) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResolutionConfig({
|
||||||
|
resolutionWidth,
|
||||||
|
resolutionHeight,
|
||||||
|
isResolutionEnabled,
|
||||||
|
isResolutionGroupEnabled,
|
||||||
|
isFullscreen,
|
||||||
|
resolutionGroup,
|
||||||
|
isMonitoring,
|
||||||
|
fpsTest,
|
||||||
|
onPresetResolution,
|
||||||
|
}: ResolutionConfigProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-end gap-2 shrink-0">
|
||||||
|
{/* 分辨率设置 */}
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
{/* 工具栏:分辨率标签 + 按钮 */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="text-xs text-default-500 shrink-0">分辨率</label>
|
||||||
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={isResolutionGroupEnabled ? "solid" : "flat"}
|
||||||
|
color={isResolutionGroupEnabled ? "primary" : "default"}
|
||||||
|
onPress={() => {
|
||||||
|
if (!isMonitoring) {
|
||||||
|
const newValue = !isResolutionGroupEnabled
|
||||||
|
fpsTest.setIsResolutionGroupEnabled(newValue)
|
||||||
|
// 启用分辨率组时,自动启用分辨率功能
|
||||||
|
if (newValue && !isResolutionEnabled) {
|
||||||
|
fpsTest.setIsResolutionEnabled(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isDisabled={isMonitoring}
|
||||||
|
className="h-5 gap-1 flex px-1.5 min-w-fit text-xs font-medium"
|
||||||
|
>
|
||||||
|
<List size={12} />
|
||||||
|
多组
|
||||||
|
</Button>
|
||||||
|
{!isResolutionGroupEnabled && (
|
||||||
|
<>
|
||||||
|
<Dropdown placement="bottom-end" className="min-w-fit">
|
||||||
|
<DropdownTrigger>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
className="h-5 min-w-[40px] px-1.5 text-xs"
|
||||||
|
isDisabled={!isResolutionEnabled || isMonitoring}
|
||||||
|
>
|
||||||
|
预设
|
||||||
|
</Button>
|
||||||
|
</DropdownTrigger>
|
||||||
|
<DropdownMenu aria-label="预设分辨率" className="">
|
||||||
|
{PRESET_RESOLUTIONS.map((preset) => (
|
||||||
|
<DropdownItem
|
||||||
|
key={preset.label}
|
||||||
|
onPress={() => onPresetResolution(preset)}
|
||||||
|
>
|
||||||
|
{preset.label}
|
||||||
|
</DropdownItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={isResolutionEnabled ? "solid" : "flat"}
|
||||||
|
color={isResolutionEnabled ? "primary" : "default"}
|
||||||
|
onPress={() => fpsTest.setIsResolutionEnabled(!isResolutionEnabled)}
|
||||||
|
isDisabled={isMonitoring}
|
||||||
|
className="h-5 min-w-[40px] px-1.5 text-xs font-medium"
|
||||||
|
>
|
||||||
|
{isResolutionEnabled ? "启用" : "关闭"}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isResolutionGroupEnabled && (
|
||||||
|
<>
|
||||||
|
<Dropdown placement="bottom-end" className="min-w-fit">
|
||||||
|
<DropdownTrigger>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
className="h-5 min-w-[40px] px-1.5 text-xs"
|
||||||
|
isDisabled={isMonitoring}
|
||||||
|
>
|
||||||
|
预设
|
||||||
|
</Button>
|
||||||
|
</DropdownTrigger>
|
||||||
|
<DropdownMenu aria-label="预设分辨率" className="">
|
||||||
|
{PRESET_RESOLUTIONS.map((preset) => (
|
||||||
|
<DropdownItem
|
||||||
|
key={preset.label}
|
||||||
|
onPress={() => {
|
||||||
|
if (!isMonitoring) {
|
||||||
|
fpsTest.addResolutionToGroup({
|
||||||
|
width: preset.width,
|
||||||
|
height: preset.height,
|
||||||
|
label: preset.label,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{preset.label}
|
||||||
|
</DropdownItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
onPress={() => {
|
||||||
|
if (!isMonitoring && resolutionWidth && resolutionHeight) {
|
||||||
|
fpsTest.addResolutionToGroup({
|
||||||
|
width: resolutionWidth,
|
||||||
|
height: resolutionHeight,
|
||||||
|
label: `${resolutionWidth}x${resolutionHeight}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isDisabled={isMonitoring || !resolutionWidth || !resolutionHeight}
|
||||||
|
className="h-5 min-w-[40px] px-1.5 text-xs"
|
||||||
|
>
|
||||||
|
添加
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* 主体:宽高输入框 + 全屏按钮(始终显示) */}
|
||||||
|
<div className="flex items-center gap-2 ">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Input
|
||||||
|
size="md"
|
||||||
|
type="number"
|
||||||
|
placeholder="宽"
|
||||||
|
value={resolutionWidth}
|
||||||
|
onValueChange={(val) => fpsTest.setResolution(val, resolutionHeight)}
|
||||||
|
isDisabled={
|
||||||
|
isResolutionGroupEnabled
|
||||||
|
? isMonitoring
|
||||||
|
: !isResolutionEnabled || isMonitoring
|
||||||
|
}
|
||||||
|
className="w-20"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-default-400">x</span>
|
||||||
|
<Input
|
||||||
|
size="md"
|
||||||
|
type="number"
|
||||||
|
placeholder="高"
|
||||||
|
value={resolutionHeight}
|
||||||
|
onValueChange={(val) => fpsTest.setResolution(resolutionWidth, val)}
|
||||||
|
isDisabled={
|
||||||
|
isResolutionGroupEnabled
|
||||||
|
? isMonitoring
|
||||||
|
: !isResolutionEnabled || isMonitoring
|
||||||
|
}
|
||||||
|
className="w-20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="md"
|
||||||
|
variant={isFullscreen ? "solid" : "flat"}
|
||||||
|
color={isFullscreen ? "primary" : "default"}
|
||||||
|
onPress={() => fpsTest.setIsFullscreen(!isFullscreen)}
|
||||||
|
isDisabled={isMonitoring || (!isResolutionEnabled && !isResolutionGroupEnabled)}
|
||||||
|
className="font-medium"
|
||||||
|
>
|
||||||
|
{isFullscreen ? "全屏" : "窗口化"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
94
src/components/cstb/FpsTest/components/TestConfigPanel.tsx
Normal file
94
src/components/cstb/FpsTest/components/TestConfigPanel.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { Tabs, Tab, Select, SelectItem, Input } from "@heroui/react"
|
||||||
|
import { BENCHMARK_MAPS } from "../constants"
|
||||||
|
import type { useFpsTestStore } from "@/store/fps_test"
|
||||||
|
|
||||||
|
interface TestConfigPanelProps {
|
||||||
|
selectedMapIndex: number
|
||||||
|
onMapIndexChange: (index: number) => void
|
||||||
|
batchTestCount: number
|
||||||
|
onBatchTestCountChange: (count: number) => void
|
||||||
|
testNote: string
|
||||||
|
onTestNoteChange: (note: string) => void
|
||||||
|
customLaunchOption: string
|
||||||
|
onCustomLaunchOptionChange: (option: string) => void
|
||||||
|
isMonitoring: boolean
|
||||||
|
fpsTest: ReturnType<typeof useFpsTestStore>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TestConfigPanel({
|
||||||
|
selectedMapIndex,
|
||||||
|
onMapIndexChange,
|
||||||
|
batchTestCount,
|
||||||
|
onBatchTestCountChange,
|
||||||
|
testNote,
|
||||||
|
onTestNoteChange,
|
||||||
|
customLaunchOption,
|
||||||
|
onCustomLaunchOptionChange,
|
||||||
|
isMonitoring,
|
||||||
|
fpsTest,
|
||||||
|
}: TestConfigPanelProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* 备注单独一行 - 放在最上面 */}
|
||||||
|
<div className="flex flex-row gap-1.5">
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<label className="text-xs text-default-500">测试地图</label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Tabs
|
||||||
|
selectedKey={String(selectedMapIndex)}
|
||||||
|
onSelectionChange={(key) => {
|
||||||
|
if (!isMonitoring) {
|
||||||
|
onMapIndexChange(Number(key))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-label="测试地图选择"
|
||||||
|
size="sm"
|
||||||
|
radius="lg"
|
||||||
|
>
|
||||||
|
{BENCHMARK_MAPS.map((map, index) => (
|
||||||
|
<Tab key={String(index)} title={map.label} />
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<label className="text-xs text-default-500">批量测试</label>
|
||||||
|
<Select
|
||||||
|
size="md"
|
||||||
|
selectedKeys={[String(batchTestCount)]}
|
||||||
|
onSelectionChange={(keys) => {
|
||||||
|
const value = Array.from(keys)[0]
|
||||||
|
if (value && !isMonitoring) {
|
||||||
|
onBatchTestCountChange(Number(value))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isDisabled={isMonitoring}
|
||||||
|
className="w-24"
|
||||||
|
aria-label="批量测试次数"
|
||||||
|
>
|
||||||
|
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((count) => (
|
||||||
|
<SelectItem
|
||||||
|
key={String(count)}
|
||||||
|
title={count === 1 ? "单次" : `${count}次`}
|
||||||
|
></SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5 flex-1">
|
||||||
|
<label className="text-xs text-default-500">备注</label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
size="md"
|
||||||
|
placeholder="输入测试备注"
|
||||||
|
value={testNote}
|
||||||
|
onValueChange={onTestNoteChange}
|
||||||
|
isDisabled={isMonitoring}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
53
src/components/cstb/FpsTest/components/TestResultDisplay.tsx
Normal file
53
src/components/cstb/FpsTest/components/TestResultDisplay.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { Chip } from "@heroui/react"
|
||||||
|
import { extractFpsMetrics } from "../utils/fps-metrics"
|
||||||
|
|
||||||
|
interface TestResultDisplayProps {
|
||||||
|
testResult: string | null
|
||||||
|
testTimestamp: string | null
|
||||||
|
isMonitoring: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TestResultDisplay({
|
||||||
|
testResult,
|
||||||
|
testTimestamp,
|
||||||
|
isMonitoring,
|
||||||
|
}: TestResultDisplayProps) {
|
||||||
|
if (!testResult || !testTimestamp) {
|
||||||
|
if (isMonitoring) {
|
||||||
|
return (
|
||||||
|
<Chip size="lg" color="primary" variant="flat" className="text-xs">
|
||||||
|
正在监听中...
|
||||||
|
</Chip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const { avg, p1 } = extractFpsMetrics(testResult)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="px-3 py-1.5 h-12 rounded-md bg-default-100 dark:bg-default-50 text-xs flex flex-col justify-center">
|
||||||
|
<div className="text-xs text-default-500">测试时间</div>
|
||||||
|
<div className="font-medium">{testTimestamp}</div>
|
||||||
|
</div>
|
||||||
|
<div className="px-3 py-1.5 h-12 rounded-md bg-default-100 dark:bg-default-50 text-xs flex items-center">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-default-500">平均帧</div>
|
||||||
|
<div className="font-medium">
|
||||||
|
{avg !== null ? `${avg.toFixed(1)}` : "N/A"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-default-500">P1低帧</div>
|
||||||
|
<div className="font-medium">
|
||||||
|
{p1 !== null ? `${p1.toFixed(1)}` : "N/A"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
156
src/components/cstb/FpsTest/components/TestResultsTable.tsx
Normal file
156
src/components/cstb/FpsTest/components/TestResultsTable.tsx
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableColumn,
|
||||||
|
TableBody,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
Button,
|
||||||
|
Tooltip,
|
||||||
|
} from "@heroui/react"
|
||||||
|
import { Delete } from "@icon-park/react"
|
||||||
|
import { addToast } from "@heroui/react"
|
||||||
|
import { NoteCell } from "./NoteCell"
|
||||||
|
import type { FpsTestResult } from "@/store/fps_test"
|
||||||
|
import type { useFpsTestStore } from "@/store/fps_test"
|
||||||
|
|
||||||
|
interface TestResultsTableProps {
|
||||||
|
results: FpsTestResult[]
|
||||||
|
fpsTest: ReturnType<typeof useFpsTestStore>
|
||||||
|
onEditNote: (resultId: string, currentNote: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TestResultsTable({
|
||||||
|
results,
|
||||||
|
fpsTest,
|
||||||
|
onEditNote,
|
||||||
|
}: TestResultsTableProps) {
|
||||||
|
return (
|
||||||
|
<div className="relative flex flex-col gap-2">
|
||||||
|
<Table
|
||||||
|
aria-label="测试结果表格"
|
||||||
|
selectionMode="none"
|
||||||
|
classNames={{
|
||||||
|
wrapper: "overflow-auto",
|
||||||
|
base: "min-h-[222px]",
|
||||||
|
table: "min-w-full",
|
||||||
|
th: "px-3 py-2 text-xs font-semibold whitespace-nowrap",
|
||||||
|
td: "px-3 py-2 text-xs",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableHeader>
|
||||||
|
<TableColumn minWidth={140}>测试时间</TableColumn>
|
||||||
|
<TableColumn width={40}>地图</TableColumn>
|
||||||
|
<TableColumn width={100}>分辨率</TableColumn>
|
||||||
|
<TableColumn width={60}>平均帧</TableColumn>
|
||||||
|
<TableColumn width={60}>P1低帧</TableColumn>
|
||||||
|
<TableColumn width={100}>CPU</TableColumn>
|
||||||
|
<TableColumn minWidth={100}>GPU</TableColumn>
|
||||||
|
<TableColumn width={80}>内存</TableColumn>
|
||||||
|
<TableColumn width={80}>内存频率</TableColumn>
|
||||||
|
<TableColumn minWidth={80}>系统版本</TableColumn>
|
||||||
|
<TableColumn width={100}>主板型号</TableColumn>
|
||||||
|
<TableColumn minWidth={80}>主板版本</TableColumn>
|
||||||
|
<TableColumn minWidth={80}>BIOS版本</TableColumn>
|
||||||
|
<TableColumn minWidth={40}>备注</TableColumn>
|
||||||
|
<TableColumn width={60} align="center">
|
||||||
|
操作
|
||||||
|
</TableColumn>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody emptyContent="暂无测试记录">
|
||||||
|
{results.map((result) => (
|
||||||
|
<TableRow key={result.id}>
|
||||||
|
<TableCell className="text-xs whitespace-nowrap">
|
||||||
|
{result.testTime}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs">
|
||||||
|
<div className="truncate">{result.mapLabel}</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs whitespace-nowrap">
|
||||||
|
{result.videoSetting
|
||||||
|
? `${result.videoSetting.defaultres}x${result.videoSetting.defaultresheight}`
|
||||||
|
: "N/A"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs">
|
||||||
|
{result.avg !== null ? `${result.avg.toFixed(1)}` : "N/A"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs">
|
||||||
|
{result.p1 !== null ? `${result.p1.toFixed(1)}` : "N/A"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs max-w-[170px]">
|
||||||
|
<Tooltip
|
||||||
|
content={result.hardwareInfo?.cpu || "N/A"}
|
||||||
|
delay={300}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<div className="truncate cursor-help">
|
||||||
|
{result.hardwareInfo?.cpu || "N/A"}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs">
|
||||||
|
<div className="truncate">{result.hardwareInfo?.gpu || "N/A"}</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs whitespace-nowrap">
|
||||||
|
{result.hardwareInfo?.memory
|
||||||
|
? `${result.hardwareInfo.memory}GB`
|
||||||
|
: "N/A"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs whitespace-nowrap">
|
||||||
|
{result.hardwareInfo?.memorySpeed
|
||||||
|
? `${result.hardwareInfo.memorySpeed}MHz`
|
||||||
|
: "N/A"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs">
|
||||||
|
<div className="truncate">{result.hardwareInfo?.os || "N/A"}</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs">
|
||||||
|
<Tooltip
|
||||||
|
content={result.hardwareInfo?.motherboardModel || "N/A"}
|
||||||
|
delay={500}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<div className="truncate cursor-help">
|
||||||
|
{result.hardwareInfo?.motherboardModel || "N/A"}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs">
|
||||||
|
<div className="truncate">{result.hardwareInfo?.motherboardVersion || "N/A"}</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs">
|
||||||
|
<div className="truncate">{result.hardwareInfo?.biosVersion || "N/A"}</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs min-w-fit">
|
||||||
|
<NoteCell
|
||||||
|
note={result.note || ""}
|
||||||
|
onEdit={() => onEditNote(result.id, result.note || "")}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
isIconOnly
|
||||||
|
variant="light"
|
||||||
|
onPress={() => {
|
||||||
|
fpsTest.removeResult(result.id)
|
||||||
|
addToast({
|
||||||
|
title: "已删除测试记录",
|
||||||
|
variant: "flat",
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
className="h-6 min-w-6"
|
||||||
|
>
|
||||||
|
<Delete size={14} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
32
src/components/cstb/FpsTest/constants.ts
Normal file
32
src/components/cstb/FpsTest/constants.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// 测试地图配置
|
||||||
|
export const BENCHMARK_MAPS = [
|
||||||
|
{
|
||||||
|
name: "de_dust2_benchmark",
|
||||||
|
workshopId: "3240880604",
|
||||||
|
map: "de_dust2_benchmark",
|
||||||
|
label: "Dust2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "de_ancient",
|
||||||
|
workshopId: "3472126051",
|
||||||
|
map: "de_ancient",
|
||||||
|
label: "Ancient",
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
|
// 测试超时时间(毫秒)
|
||||||
|
export const TEST_TIMEOUT = 200000 // 200秒
|
||||||
|
|
||||||
|
// 预设分辨率列表
|
||||||
|
export const PRESET_RESOLUTIONS = [
|
||||||
|
{ width: "800", height: "600", label: "800x600" },
|
||||||
|
{ width: "1024", height: "768", label: "1024x768" },
|
||||||
|
{ width: "1280", height: "960", label: "1280x960" },
|
||||||
|
{ width: "1440", height: "1080", label: "1440x1080" },
|
||||||
|
{ width: "1920", height: "1080", label: "1920x1080" },
|
||||||
|
{ width: "1920", height: "1440", label: "1920x1440" },
|
||||||
|
{ width: "2560", height: "1440", label: "2560x1440" },
|
||||||
|
{ width: "2880", height: "2160", label: "2880x2160" },
|
||||||
|
{ width: "3840", height: "2160", label: "3840x2160" },
|
||||||
|
] as const
|
||||||
|
|
||||||
3
src/components/cstb/FpsTest/hooks/useGameMonitor.ts
Normal file
3
src/components/cstb/FpsTest/hooks/useGameMonitor.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// 使用全局游戏状态监控,避免重复检测
|
||||||
|
export { useGlobalGameMonitor as useGameMonitor } from "@/hooks/useGlobalGameMonitor"
|
||||||
|
|
||||||
41
src/components/cstb/FpsTest/hooks/useHardwareInfo.ts
Normal file
41
src/components/cstb/FpsTest/hooks/useHardwareInfo.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { useEffect } from "react"
|
||||||
|
import { useHardwareStore } from "@/store/hardware"
|
||||||
|
import type { AllSystemInfo } from "tauri-plugin-system-info-api"
|
||||||
|
import type {
|
||||||
|
GpuInfo,
|
||||||
|
ComputerInfo,
|
||||||
|
MemoryInfo,
|
||||||
|
MonitorInfo,
|
||||||
|
MotherboardInfo,
|
||||||
|
} from "@/store/hardware"
|
||||||
|
|
||||||
|
export interface HardwareInfoWithGpu {
|
||||||
|
systemInfo: AllSystemInfo | null
|
||||||
|
gpuInfo: GpuInfo | null
|
||||||
|
computerInfo: ComputerInfo | null
|
||||||
|
memoryInfo: MemoryInfo[]
|
||||||
|
monitorInfo: MonitorInfo[]
|
||||||
|
motherboardInfo: MotherboardInfo | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useHardwareInfo() {
|
||||||
|
const { state, fetchHardwareInfo } = useHardwareStore()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 如果数据不存在,则加载数据(store 初始化时已经加载,这里只是确保)
|
||||||
|
if (!state.allSysData) {
|
||||||
|
void fetchHardwareInfo()
|
||||||
|
}
|
||||||
|
}, []) // 只在组件挂载时执行一次
|
||||||
|
|
||||||
|
// 将 store 中的数据转换为兼容的格式
|
||||||
|
return {
|
||||||
|
systemInfo: state.allSysData,
|
||||||
|
gpuInfo: state.gpuInfo,
|
||||||
|
computerInfo: state.computerInfo,
|
||||||
|
memoryInfo: state.memoryInfo,
|
||||||
|
monitorInfo: state.monitorInfo,
|
||||||
|
motherboardInfo: state.motherboardInfo,
|
||||||
|
} as HardwareInfoWithGpu
|
||||||
|
}
|
||||||
|
|
||||||
113
src/components/cstb/FpsTest/hooks/useTestMonitor.ts
Normal file
113
src/components/cstb/FpsTest/hooks/useTestMonitor.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { useEffect, useRef } from "react"
|
||||||
|
import { invoke } from "@tauri-apps/api/core"
|
||||||
|
import { addToast } from "@heroui/react"
|
||||||
|
import { readResult, type ReadResultParams } from "../services/resultReader"
|
||||||
|
import { TEST_TIMEOUT } from "../constants"
|
||||||
|
|
||||||
|
export interface UseTestMonitorParams {
|
||||||
|
isMonitoring: boolean
|
||||||
|
cs2Dir: string | null
|
||||||
|
readResultParams: ReadResultParams
|
||||||
|
testStartTimestamp: string | null
|
||||||
|
testStartTime: number | null
|
||||||
|
autoCloseGame: boolean
|
||||||
|
onTestComplete: () => void
|
||||||
|
onTestTimeout: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTestMonitor(params: UseTestMonitorParams) {
|
||||||
|
const {
|
||||||
|
isMonitoring,
|
||||||
|
cs2Dir,
|
||||||
|
readResultParams,
|
||||||
|
testStartTimestamp,
|
||||||
|
testStartTime,
|
||||||
|
autoCloseGame,
|
||||||
|
onTestComplete,
|
||||||
|
onTestTimeout,
|
||||||
|
} = params
|
||||||
|
|
||||||
|
const monitoringIntervalRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
|
|
||||||
|
// 开始监控文件更新
|
||||||
|
useEffect(() => {
|
||||||
|
if (isMonitoring && cs2Dir) {
|
||||||
|
// 每2秒检查一次文件更新
|
||||||
|
monitoringIntervalRef.current = setInterval(async () => {
|
||||||
|
const success = await readResult(readResultParams, true) // 静默读取
|
||||||
|
if (success) {
|
||||||
|
// 读取成功,调用完成回调
|
||||||
|
onTestComplete()
|
||||||
|
// 停止监控
|
||||||
|
if (monitoringIntervalRef.current) {
|
||||||
|
clearInterval(monitoringIntervalRef.current)
|
||||||
|
monitoringIntervalRef.current = null
|
||||||
|
}
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current)
|
||||||
|
timeoutRef.current = null
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}, 2000) // 每2秒检查一次
|
||||||
|
|
||||||
|
// 设置超时
|
||||||
|
if (testStartTime) {
|
||||||
|
timeoutRef.current = setTimeout(() => {
|
||||||
|
// 超时,认为测试失败
|
||||||
|
if (monitoringIntervalRef.current) {
|
||||||
|
clearInterval(monitoringIntervalRef.current)
|
||||||
|
monitoringIntervalRef.current = null
|
||||||
|
}
|
||||||
|
onTestTimeout()
|
||||||
|
}, TEST_TIMEOUT)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 停止监控
|
||||||
|
if (monitoringIntervalRef.current) {
|
||||||
|
clearInterval(monitoringIntervalRef.current)
|
||||||
|
monitoringIntervalRef.current = null
|
||||||
|
}
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current)
|
||||||
|
timeoutRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理函数
|
||||||
|
return () => {
|
||||||
|
if (monitoringIntervalRef.current) {
|
||||||
|
clearInterval(monitoringIntervalRef.current)
|
||||||
|
monitoringIntervalRef.current = null
|
||||||
|
}
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current)
|
||||||
|
timeoutRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
isMonitoring,
|
||||||
|
cs2Dir,
|
||||||
|
readResultParams,
|
||||||
|
testStartTimestamp,
|
||||||
|
testStartTime,
|
||||||
|
autoCloseGame,
|
||||||
|
onTestComplete,
|
||||||
|
onTestTimeout,
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
stopMonitoring: () => {
|
||||||
|
if (monitoringIntervalRef.current) {
|
||||||
|
clearInterval(monitoringIntervalRef.current)
|
||||||
|
monitoringIntervalRef.current = null
|
||||||
|
}
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current)
|
||||||
|
timeoutRef.current = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
6
src/components/cstb/FpsTest/index.tsx
Normal file
6
src/components/cstb/FpsTest/index.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
"use client"
|
||||||
|
// 导出重构后的FpsTest组件
|
||||||
|
// 主组件文件已重构,使用提取的模块
|
||||||
|
|
||||||
|
export { FpsTest } from "../FpsTest"
|
||||||
|
|
||||||
287
src/components/cstb/FpsTest/services/resultReader.ts
Normal file
287
src/components/cstb/FpsTest/services/resultReader.ts
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
import { invoke } from "@tauri-apps/api/core"
|
||||||
|
import { addToast } from "@heroui/react"
|
||||||
|
import { parseVProfReport } from "../utils/vprof-parser"
|
||||||
|
import { compareTimestamps } from "../utils/timestamp"
|
||||||
|
import { extractFpsMetrics } from "../utils/fps-metrics"
|
||||||
|
import type { useSteamStore } from "@/store/steam"
|
||||||
|
import type { useToolStore } from "@/store/tool"
|
||||||
|
import type { useFpsTestStore } from "@/store/fps_test"
|
||||||
|
import type { AllSystemInfo } from "tauri-plugin-system-info-api"
|
||||||
|
import { BENCHMARK_MAPS } from "../constants"
|
||||||
|
|
||||||
|
export interface ReadResultParams {
|
||||||
|
steam: ReturnType<typeof useSteamStore>
|
||||||
|
tool: ReturnType<typeof useToolStore>
|
||||||
|
fpsTest: ReturnType<typeof useFpsTestStore>
|
||||||
|
selectedMapIndex: number
|
||||||
|
hardwareInfo: AllSystemInfo | null
|
||||||
|
testNote: string
|
||||||
|
batchTestProgress: { current: number; total: number } | null
|
||||||
|
currentTestResolution: { width: string; height: string; label: string } | null
|
||||||
|
resolutionGroupInfo: {
|
||||||
|
resIndex: number
|
||||||
|
totalResolutions: number
|
||||||
|
totalTestCount: number
|
||||||
|
currentBatchIndex: number
|
||||||
|
batchCount: number
|
||||||
|
} | null
|
||||||
|
isResolutionGroupEnabled: boolean
|
||||||
|
testStartTimestamp: string | null
|
||||||
|
lastTestTimestamp: React.MutableRefObject<string | null>
|
||||||
|
onResultRead: (data: {
|
||||||
|
timestamp: string
|
||||||
|
data: string
|
||||||
|
avg: number | null
|
||||||
|
p1: number | null
|
||||||
|
}) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readResult(
|
||||||
|
params: ReadResultParams,
|
||||||
|
silent = false
|
||||||
|
): Promise<boolean> {
|
||||||
|
const {
|
||||||
|
steam,
|
||||||
|
tool,
|
||||||
|
fpsTest,
|
||||||
|
selectedMapIndex,
|
||||||
|
hardwareInfo,
|
||||||
|
testNote,
|
||||||
|
batchTestProgress,
|
||||||
|
currentTestResolution,
|
||||||
|
resolutionGroupInfo,
|
||||||
|
isResolutionGroupEnabled,
|
||||||
|
testStartTimestamp,
|
||||||
|
lastTestTimestamp,
|
||||||
|
onResultRead,
|
||||||
|
} = params
|
||||||
|
|
||||||
|
if (!steam.state.cs2Dir) {
|
||||||
|
if (!silent) {
|
||||||
|
addToast({ title: "请先配置 CS2 路径", variant: "flat" })
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取 console.log 路径
|
||||||
|
let consoleLogPath: string
|
||||||
|
try {
|
||||||
|
consoleLogPath = await invoke<string>("get_console_log_path", {
|
||||||
|
csPath: steam.state.cs2Dir,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取控制台日志路径失败:", error)
|
||||||
|
if (!silent) {
|
||||||
|
addToast({
|
||||||
|
title: "获取控制台日志路径失败",
|
||||||
|
color: "warning",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取 VProf 报告
|
||||||
|
let report: string
|
||||||
|
try {
|
||||||
|
report = await invoke<string>("read_vprof_report", {
|
||||||
|
consoleLogPath: consoleLogPath,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("读取性能报告失败:", error)
|
||||||
|
if (!silent) {
|
||||||
|
addToast({
|
||||||
|
title: "读取性能报告失败",
|
||||||
|
color: "warning",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (report && report.trim().length > 0) {
|
||||||
|
const parsed = parseVProfReport(report)
|
||||||
|
if (parsed) {
|
||||||
|
// 如果设置了测试开始时间且是自动监听(silent=true),验证报告时间戳是否晚于测试开始时间
|
||||||
|
// 手动读取(silent=false)时允许读取任何结果
|
||||||
|
if (silent && testStartTimestamp) {
|
||||||
|
// 如果报告时间戳早于或等于测试开始时间,则视为旧数据,忽略
|
||||||
|
if (!compareTimestamps(parsed.timestamp, testStartTimestamp)) {
|
||||||
|
// 这是旧数据,不处理
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存最后一次测试的时间戳(用于平均值记录)
|
||||||
|
lastTestTimestamp.current = parsed.timestamp
|
||||||
|
|
||||||
|
// 提取 avg 和 p1 值
|
||||||
|
const { avg, p1 } = extractFpsMetrics(parsed.data)
|
||||||
|
|
||||||
|
// 保存测试结果
|
||||||
|
const now = new Date()
|
||||||
|
const testDate = now.toISOString()
|
||||||
|
const mapConfig = BENCHMARK_MAPS[selectedMapIndex]
|
||||||
|
|
||||||
|
// 测试结束后读取视频设置(检测分辨率)
|
||||||
|
if (steam.state.steamDirValid && steam.currentUser()) {
|
||||||
|
await tool.getVideoConfig(steam.state.steamDir, steam.currentUser()?.steam_id32 || 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用读取到的视频设置(测试结束后读取的)
|
||||||
|
const currentVideoSetting = tool.store.state.videoSetting
|
||||||
|
|
||||||
|
// 如果是批量测试,添加带批量标识和分辨率信息的备注
|
||||||
|
if (batchTestProgress) {
|
||||||
|
let batchNote = ""
|
||||||
|
if (currentTestResolution) {
|
||||||
|
batchNote = `[${currentTestResolution.label}]`
|
||||||
|
}
|
||||||
|
if (testNote) {
|
||||||
|
batchNote = batchNote ? `${testNote} ${batchNote}` : testNote
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果启用了分辨率组,使用新的备注格式:[分辨率] [批量当前测试/该分辨率批量总数]
|
||||||
|
if (resolutionGroupInfo && isResolutionGroupEnabled) {
|
||||||
|
const { currentBatchIndex, batchCount } = resolutionGroupInfo
|
||||||
|
const batchInfo = `[批量${currentBatchIndex}/${batchCount}]`
|
||||||
|
batchNote = batchNote ? `${batchNote} ${batchInfo}` : batchInfo
|
||||||
|
} else {
|
||||||
|
// 普通批量测试,使用原来的格式
|
||||||
|
batchNote = batchNote
|
||||||
|
? `${batchNote} [批量${batchTestProgress.current}/${batchTestProgress.total}]`
|
||||||
|
: `[批量${batchTestProgress.current}/${batchTestProgress.total}]`
|
||||||
|
}
|
||||||
|
|
||||||
|
fpsTest.addResult({
|
||||||
|
id: `${now.getTime()}-${Math.random().toString(36).slice(2, 11)}`,
|
||||||
|
testTime: parsed.timestamp,
|
||||||
|
testDate,
|
||||||
|
mapName: mapConfig?.name || "unknown",
|
||||||
|
mapLabel: mapConfig?.label || "未知地图",
|
||||||
|
avg,
|
||||||
|
p1,
|
||||||
|
rawResult: parsed.data,
|
||||||
|
videoSetting: currentVideoSetting,
|
||||||
|
hardwareInfo: hardwareInfo
|
||||||
|
? {
|
||||||
|
cpu: hardwareInfo.cpus[0]?.brand || null,
|
||||||
|
cpuCount: hardwareInfo.cpu_count || null,
|
||||||
|
os:
|
||||||
|
hardwareInfo.name && hardwareInfo.os_version
|
||||||
|
? `${hardwareInfo.name} ${hardwareInfo.os_version}`
|
||||||
|
: null,
|
||||||
|
memory: hardwareInfo.total_memory
|
||||||
|
? Math.round(hardwareInfo.total_memory / 1024 / 1024 / 1024)
|
||||||
|
: null,
|
||||||
|
memoryManufacturer: null,
|
||||||
|
memorySpeed: null,
|
||||||
|
memoryDefaultSpeed: null,
|
||||||
|
gpu: null,
|
||||||
|
monitor: null,
|
||||||
|
monitorManufacturer: null,
|
||||||
|
monitorModel: null,
|
||||||
|
motherboardModel: null,
|
||||||
|
motherboardVersion: null,
|
||||||
|
biosVersion: null,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
note: batchNote,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 单次测试,添加分辨率信息到备注
|
||||||
|
let singleNote = testNote
|
||||||
|
if (currentTestResolution) {
|
||||||
|
const resolutionNote = `[${currentTestResolution.label}]`
|
||||||
|
singleNote = singleNote ? `${testNote} ${resolutionNote}` : resolutionNote
|
||||||
|
}
|
||||||
|
|
||||||
|
fpsTest.addResult({
|
||||||
|
id: `${now.getTime()}-${Math.random().toString(36).slice(2, 11)}`,
|
||||||
|
testTime: parsed.timestamp,
|
||||||
|
testDate,
|
||||||
|
mapName: mapConfig?.name || "unknown",
|
||||||
|
mapLabel: mapConfig?.label || "未知地图",
|
||||||
|
avg,
|
||||||
|
p1,
|
||||||
|
rawResult: parsed.data,
|
||||||
|
videoSetting: currentVideoSetting,
|
||||||
|
hardwareInfo: hardwareInfo
|
||||||
|
? {
|
||||||
|
cpu: hardwareInfo.cpus[0]?.brand || null,
|
||||||
|
cpuCount: hardwareInfo.cpu_count || null,
|
||||||
|
os:
|
||||||
|
hardwareInfo.name && hardwareInfo.os_version
|
||||||
|
? `${hardwareInfo.name} ${hardwareInfo.os_version}`
|
||||||
|
: null,
|
||||||
|
memory: hardwareInfo.total_memory
|
||||||
|
? Math.round(hardwareInfo.total_memory / 1024 / 1024 / 1024)
|
||||||
|
: null,
|
||||||
|
memoryManufacturer: null,
|
||||||
|
memorySpeed: null,
|
||||||
|
memoryDefaultSpeed: null,
|
||||||
|
gpu: null,
|
||||||
|
monitor: null,
|
||||||
|
monitorManufacturer: null,
|
||||||
|
monitorModel: null,
|
||||||
|
motherboardModel: null,
|
||||||
|
motherboardVersion: null,
|
||||||
|
biosVersion: null,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
note: singleNote, // 保存备注(包含分辨率信息)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用回调函数
|
||||||
|
onResultRead({
|
||||||
|
timestamp: parsed.timestamp,
|
||||||
|
data: parsed.data,
|
||||||
|
avg,
|
||||||
|
p1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!silent) {
|
||||||
|
if (avg !== null || p1 !== null) {
|
||||||
|
addToast({
|
||||||
|
title: `已读取并保存测试结果${
|
||||||
|
avg !== null ? ` (avg: ${avg.toFixed(1)} FPS)` : ""
|
||||||
|
}${p1 !== null ? ` (p1: ${p1.toFixed(1)} FPS)` : ""}`,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
addToast({ title: "已读取并保存测试结果(未能提取帧数数据)" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果启用了自动关闭游戏,则关闭游戏
|
||||||
|
if (tool.state.autoCloseGame) {
|
||||||
|
setTimeout(() => {
|
||||||
|
void invoke("kill_game").catch(() => {})
|
||||||
|
}, 2000) // 延迟2秒关闭,让用户看到结果
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
} else if (!silent) {
|
||||||
|
addToast({
|
||||||
|
title: "未能解析测试结果",
|
||||||
|
variant: "flat",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (!silent) {
|
||||||
|
addToast({
|
||||||
|
title: "未能读取测试结果,请确保测试已完成",
|
||||||
|
variant: "flat",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
} catch (error) {
|
||||||
|
if (!silent) {
|
||||||
|
console.error("读取结果失败:", error)
|
||||||
|
addToast({
|
||||||
|
title: `读取结果失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
variant: "flat",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
172
src/components/cstb/FpsTest/services/testRunner.ts
Normal file
172
src/components/cstb/FpsTest/services/testRunner.ts
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import { invoke } from "@tauri-apps/api/core"
|
||||||
|
import { addToast } from "@heroui/react"
|
||||||
|
import { BENCHMARK_MAPS, TEST_TIMEOUT } from "../constants"
|
||||||
|
import { formatCurrentTimestamp } from "../utils/timestamp"
|
||||||
|
import type { useSteamStore } from "@/store/steam"
|
||||||
|
import type { useToolStore } from "@/store/tool"
|
||||||
|
import type { Resolution } from "../types"
|
||||||
|
|
||||||
|
export interface RunSingleTestParams {
|
||||||
|
steam: ReturnType<typeof useSteamStore>
|
||||||
|
tool: ReturnType<typeof useToolStore>
|
||||||
|
selectedMapIndex: number
|
||||||
|
resolutionWidth: string
|
||||||
|
resolutionHeight: string
|
||||||
|
isResolutionEnabled: boolean
|
||||||
|
isResolutionGroupEnabled: boolean
|
||||||
|
isFullscreen: boolean
|
||||||
|
customLaunchOption: string
|
||||||
|
autoCloseGame: boolean
|
||||||
|
checkGameRunning: () => Promise<boolean>
|
||||||
|
resolution?: Resolution
|
||||||
|
testIndex?: number
|
||||||
|
totalTests?: number
|
||||||
|
onTestStart: (timestamp: string, startTime: number, resolution: Resolution | null) => void
|
||||||
|
onTestComplete: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RunSingleTestResult {
|
||||||
|
success: boolean
|
||||||
|
testStartTimestamp: string
|
||||||
|
testStartTime: number
|
||||||
|
resolution: Resolution
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runSingleTest(
|
||||||
|
params: RunSingleTestParams
|
||||||
|
): Promise<RunSingleTestResult | null> {
|
||||||
|
const {
|
||||||
|
steam,
|
||||||
|
tool,
|
||||||
|
selectedMapIndex,
|
||||||
|
resolutionWidth,
|
||||||
|
resolutionHeight,
|
||||||
|
isResolutionEnabled,
|
||||||
|
isResolutionGroupEnabled,
|
||||||
|
isFullscreen,
|
||||||
|
customLaunchOption,
|
||||||
|
autoCloseGame,
|
||||||
|
checkGameRunning,
|
||||||
|
resolution,
|
||||||
|
testIndex,
|
||||||
|
totalTests,
|
||||||
|
onTestStart,
|
||||||
|
} = params
|
||||||
|
|
||||||
|
// 验证路径是否存在且有效
|
||||||
|
if (!steam.state.steamDir || !steam.state.cs2Dir) {
|
||||||
|
addToast({
|
||||||
|
title: "Steam 或 CS2 路径未设置,请先配置路径",
|
||||||
|
color: "warning",
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 Steam 路径是否有效
|
||||||
|
if (!steam.state.steamDirValid) {
|
||||||
|
addToast({
|
||||||
|
title: "Steam 路径无效,请检查路径设置",
|
||||||
|
color: "warning",
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapConfig = BENCHMARK_MAPS[selectedMapIndex]
|
||||||
|
if (!mapConfig) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果启用了自动关闭游戏,检测并关闭正在运行的游戏
|
||||||
|
if (autoCloseGame) {
|
||||||
|
const gameRunning = await checkGameRunning()
|
||||||
|
if (gameRunning) {
|
||||||
|
try {
|
||||||
|
await invoke("kill_game")
|
||||||
|
// 等待一下确保游戏关闭
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||||
|
} catch (error) {
|
||||||
|
console.error("关闭游戏失败:", error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录测试开始时间戳(格式:MM/DD HH:mm:ss)
|
||||||
|
const testStartTimestamp = formatCurrentTimestamp()
|
||||||
|
const testStartTime = Date.now()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 构建启动参数:基础参数 + 分辨率和全屏设置 + 自定义启动项(如果有)
|
||||||
|
let baseLaunchOption = `-allow_third_party_software -condebug -conclearlog +map_workshop ${mapConfig.workshopId} ${mapConfig.map}`
|
||||||
|
|
||||||
|
// 使用传入的分辨率,如果没有则使用store中的分辨率
|
||||||
|
const currentResolution: Resolution = resolution || {
|
||||||
|
width: resolutionWidth,
|
||||||
|
height: resolutionHeight,
|
||||||
|
label: `${resolutionWidth}x${resolutionHeight}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加分辨率设置(如果启用分辨率功能或分辨率组)
|
||||||
|
if (isResolutionEnabled || isResolutionGroupEnabled) {
|
||||||
|
// 添加分辨率设置(如果有设置)
|
||||||
|
if (currentResolution.width && currentResolution.height) {
|
||||||
|
baseLaunchOption += ` -w ${currentResolution.width} -h ${currentResolution.height}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加全屏/窗口化设置(独立控制,不依赖游戏设置)
|
||||||
|
if (isFullscreen) {
|
||||||
|
baseLaunchOption += ` -fullscreen`
|
||||||
|
} else {
|
||||||
|
baseLaunchOption += ` -sw`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加自定义启动项(如果有,开头加空格避免粘连)
|
||||||
|
const launchOption = customLaunchOption.trim()
|
||||||
|
? `${baseLaunchOption} ${customLaunchOption.trim()}`
|
||||||
|
: baseLaunchOption
|
||||||
|
|
||||||
|
// 启动游戏(强制使用worldwide国际服)
|
||||||
|
try {
|
||||||
|
await invoke("launch_game", {
|
||||||
|
steamPath: `${steam.state.steamDir}\\steam.exe`,
|
||||||
|
launchOption: launchOption,
|
||||||
|
server: "worldwide",
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("启动游戏失败:", error)
|
||||||
|
addToast({
|
||||||
|
title: `启动游戏失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
color: "danger",
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用测试开始回调
|
||||||
|
onTestStart(testStartTimestamp, testStartTime, currentResolution)
|
||||||
|
|
||||||
|
const resolutionInfo = currentResolution ? ` (${currentResolution.label})` : ""
|
||||||
|
if (totalTests && totalTests > 1) {
|
||||||
|
addToast({
|
||||||
|
title: `批量测试 ${testIndex}/${totalTests}${resolutionInfo}:已启动 ${mapConfig.label} 测试,正在自动监听结果...`,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
addToast({ title: `已启动 ${mapConfig.label} 测试${resolutionInfo},正在自动监听结果...` })
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
testStartTimestamp,
|
||||||
|
testStartTime,
|
||||||
|
resolution: currentResolution,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("启动测试失败:", error)
|
||||||
|
addToast({
|
||||||
|
title: `启动测试失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
variant: "flat",
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
25
src/components/cstb/FpsTest/types.ts
Normal file
25
src/components/cstb/FpsTest/types.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// 类型定义文件
|
||||||
|
export type Resolution = {
|
||||||
|
width: string
|
||||||
|
height: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BatchTestProgress = {
|
||||||
|
current: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FpsMetrics = {
|
||||||
|
avg: number | null
|
||||||
|
p1: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResolutionGroupInfo = {
|
||||||
|
resIndex: number
|
||||||
|
totalResolutions: number
|
||||||
|
totalTestCount: number
|
||||||
|
currentBatchIndex: number
|
||||||
|
batchCount: number
|
||||||
|
}
|
||||||
|
|
||||||
235
src/components/cstb/FpsTest/utils/csv-export.ts
Normal file
235
src/components/cstb/FpsTest/utils/csv-export.ts
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import { save } from "@tauri-apps/plugin-dialog"
|
||||||
|
import { writeTextFile } from "@tauri-apps/plugin-fs"
|
||||||
|
import { addToast } from "@heroui/react"
|
||||||
|
import type { VideoSetting } from "@/store/tool"
|
||||||
|
import type { FpsTestResult } from "@/store/fps_test"
|
||||||
|
|
||||||
|
// 格式化视频设置摘要
|
||||||
|
export function formatVideoSettingSummary(
|
||||||
|
videoSetting: VideoSetting | null
|
||||||
|
): string {
|
||||||
|
if (!videoSetting) return "N/A"
|
||||||
|
const resolution = `${videoSetting.defaultres}x${videoSetting.defaultresheight}`
|
||||||
|
const refreshRate =
|
||||||
|
videoSetting.refreshrate_denominator === "1"
|
||||||
|
? videoSetting.refreshrate_numerator
|
||||||
|
: `${videoSetting.refreshrate_numerator}/${videoSetting.refreshrate_denominator}`
|
||||||
|
const msaa = videoSetting.msaa_samples === "0" ? "无" : `${videoSetting.msaa_samples}x`
|
||||||
|
return `${resolution}@${refreshRate}Hz, MSAA:${msaa}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出所有测试结果CSV
|
||||||
|
export async function handleExportCSV(
|
||||||
|
results: FpsTestResult[]
|
||||||
|
) {
|
||||||
|
if (results.length === 0) {
|
||||||
|
addToast({ title: "没有测试数据可导出", color: "warning" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 构建CSV内容
|
||||||
|
const headers = [
|
||||||
|
"测试时间",
|
||||||
|
"测试地图",
|
||||||
|
"平均帧",
|
||||||
|
"P1低帧",
|
||||||
|
"CPU",
|
||||||
|
"系统版本",
|
||||||
|
"GPU",
|
||||||
|
"内存(GB)",
|
||||||
|
"内存频率(MHz)",
|
||||||
|
"主板型号",
|
||||||
|
"主板版本",
|
||||||
|
"BIOS版本",
|
||||||
|
"分辨率",
|
||||||
|
"视频设置",
|
||||||
|
"备注",
|
||||||
|
"光影质量",
|
||||||
|
"纹理过滤质量",
|
||||||
|
"多重采样抗锯齿",
|
||||||
|
"CMAA抗锯齿",
|
||||||
|
"阴影质量",
|
||||||
|
"动态阴影",
|
||||||
|
"纹理细节",
|
||||||
|
"粒子细节",
|
||||||
|
"环境光遮蔽",
|
||||||
|
"HDR细节",
|
||||||
|
"FSR细节",
|
||||||
|
]
|
||||||
|
|
||||||
|
const csvRows = [headers.join(",")]
|
||||||
|
|
||||||
|
for (const result of results) {
|
||||||
|
const row = [
|
||||||
|
`"${result.testTime}"`,
|
||||||
|
`"${result.mapLabel}"`,
|
||||||
|
result.avg !== null ? result.avg.toFixed(1) : "N/A",
|
||||||
|
result.p1 !== null ? result.p1.toFixed(1) : "N/A",
|
||||||
|
`"${result.hardwareInfo?.cpu || "N/A"}"`,
|
||||||
|
`"${result.hardwareInfo?.os || "N/A"}"`,
|
||||||
|
`"${result.hardwareInfo?.gpu || "N/A"}"`,
|
||||||
|
result.hardwareInfo?.memory ? result.hardwareInfo.memory.toString() : "N/A",
|
||||||
|
result.hardwareInfo?.memorySpeed ? result.hardwareInfo.memorySpeed.toString() : "N/A",
|
||||||
|
`"${result.hardwareInfo?.motherboardModel || "N/A"}"`,
|
||||||
|
`"${result.hardwareInfo?.motherboardVersion || "N/A"}"`,
|
||||||
|
`"${result.hardwareInfo?.biosVersion || "N/A"}"`,
|
||||||
|
result.videoSetting
|
||||||
|
? `${result.videoSetting.defaultres}x${result.videoSetting.defaultresheight}`
|
||||||
|
: "N/A",
|
||||||
|
`"${formatVideoSettingSummary(result.videoSetting)}"`,
|
||||||
|
`"${result.note || ""}"`,
|
||||||
|
result.videoSetting?.shaderquality || "N/A",
|
||||||
|
result.videoSetting?.r_texturefilteringquality || "N/A",
|
||||||
|
result.videoSetting?.msaa_samples || "N/A",
|
||||||
|
result.videoSetting?.r_csgo_cmaa_enable || "N/A",
|
||||||
|
result.videoSetting?.videocfg_shadow_quality || "N/A",
|
||||||
|
result.videoSetting?.videocfg_dynamic_shadows || "N/A",
|
||||||
|
result.videoSetting?.videocfg_texture_detail || "N/A",
|
||||||
|
result.videoSetting?.videocfg_particle_detail || "N/A",
|
||||||
|
result.videoSetting?.videocfg_ao_detail || "N/A",
|
||||||
|
result.videoSetting?.videocfg_hdr_detail || "N/A",
|
||||||
|
result.videoSetting?.videocfg_fsr_detail || "N/A",
|
||||||
|
]
|
||||||
|
csvRows.push(row.join(","))
|
||||||
|
}
|
||||||
|
|
||||||
|
const csvContent = csvRows.join("\n")
|
||||||
|
|
||||||
|
// 添加UTF-8 BOM以确保Excel等软件正确识别编码
|
||||||
|
const csvContentWithBOM = "\uFEFF" + csvContent
|
||||||
|
|
||||||
|
// 使用文件保存对话框
|
||||||
|
const filePath = await save({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: "CSV",
|
||||||
|
extensions: ["csv"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaultPath: `fps_test_results_${new Date().toISOString().split("T")[0]}.csv`,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (filePath) {
|
||||||
|
await writeTextFile(filePath, csvContentWithBOM)
|
||||||
|
addToast({ title: "导出成功", color: "success" })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("导出CSV失败:", error)
|
||||||
|
addToast({
|
||||||
|
title: `导出失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
color: "danger",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅导出平均结果CSV
|
||||||
|
export async function handleExportAverageCSV(
|
||||||
|
results: FpsTestResult[]
|
||||||
|
) {
|
||||||
|
// 过滤备注中包含"平均"的结果
|
||||||
|
const averageResults = results.filter(
|
||||||
|
(result) => result.note && result.note.includes("平均")
|
||||||
|
)
|
||||||
|
|
||||||
|
if (averageResults.length === 0) {
|
||||||
|
addToast({ title: "没有平均结果数据可导出", color: "warning" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 构建CSV内容
|
||||||
|
const headers = [
|
||||||
|
"测试时间",
|
||||||
|
"测试地图",
|
||||||
|
"平均帧",
|
||||||
|
"P1低帧",
|
||||||
|
"CPU",
|
||||||
|
"系统版本",
|
||||||
|
"GPU",
|
||||||
|
"内存(GB)",
|
||||||
|
"内存频率(MHz)",
|
||||||
|
"主板型号",
|
||||||
|
"主板版本",
|
||||||
|
"BIOS版本",
|
||||||
|
"分辨率",
|
||||||
|
"视频设置",
|
||||||
|
"备注",
|
||||||
|
"光影质量",
|
||||||
|
"纹理过滤质量",
|
||||||
|
"多重采样抗锯齿",
|
||||||
|
"CMAA抗锯齿",
|
||||||
|
"阴影质量",
|
||||||
|
"动态阴影",
|
||||||
|
"纹理细节",
|
||||||
|
"粒子细节",
|
||||||
|
"环境光遮蔽",
|
||||||
|
"HDR细节",
|
||||||
|
"FSR细节",
|
||||||
|
]
|
||||||
|
|
||||||
|
const csvRows = [headers.join(",")]
|
||||||
|
|
||||||
|
for (const result of averageResults) {
|
||||||
|
const row = [
|
||||||
|
`"${result.testTime}"`,
|
||||||
|
`"${result.mapLabel}"`,
|
||||||
|
result.avg !== null ? result.avg.toFixed(1) : "N/A",
|
||||||
|
result.p1 !== null ? result.p1.toFixed(1) : "N/A",
|
||||||
|
`"${result.hardwareInfo?.cpu || "N/A"}"`,
|
||||||
|
`"${result.hardwareInfo?.os || "N/A"}"`,
|
||||||
|
`"${result.hardwareInfo?.gpu || "N/A"}"`,
|
||||||
|
result.hardwareInfo?.memory ? result.hardwareInfo.memory.toString() : "N/A",
|
||||||
|
result.hardwareInfo?.memorySpeed ? result.hardwareInfo.memorySpeed.toString() : "N/A",
|
||||||
|
`"${result.hardwareInfo?.motherboardModel || "N/A"}"`,
|
||||||
|
`"${result.hardwareInfo?.motherboardVersion || "N/A"}"`,
|
||||||
|
`"${result.hardwareInfo?.biosVersion || "N/A"}"`,
|
||||||
|
result.videoSetting
|
||||||
|
? `${result.videoSetting.defaultres}x${result.videoSetting.defaultresheight}`
|
||||||
|
: "N/A",
|
||||||
|
`"${formatVideoSettingSummary(result.videoSetting)}"`,
|
||||||
|
`"${result.note || ""}"`,
|
||||||
|
result.videoSetting?.shaderquality || "N/A",
|
||||||
|
result.videoSetting?.r_texturefilteringquality || "N/A",
|
||||||
|
result.videoSetting?.msaa_samples || "N/A",
|
||||||
|
result.videoSetting?.r_csgo_cmaa_enable || "N/A",
|
||||||
|
result.videoSetting?.videocfg_shadow_quality || "N/A",
|
||||||
|
result.videoSetting?.videocfg_dynamic_shadows || "N/A",
|
||||||
|
result.videoSetting?.videocfg_texture_detail || "N/A",
|
||||||
|
result.videoSetting?.videocfg_particle_detail || "N/A",
|
||||||
|
result.videoSetting?.videocfg_ao_detail || "N/A",
|
||||||
|
result.videoSetting?.videocfg_hdr_detail || "N/A",
|
||||||
|
result.videoSetting?.videocfg_fsr_detail || "N/A",
|
||||||
|
]
|
||||||
|
csvRows.push(row.join(","))
|
||||||
|
}
|
||||||
|
|
||||||
|
const csvContent = csvRows.join("\n")
|
||||||
|
|
||||||
|
// 添加UTF-8 BOM以确保Excel等软件正确识别编码
|
||||||
|
const csvContentWithBOM = "\uFEFF" + csvContent
|
||||||
|
|
||||||
|
// 使用文件保存对话框
|
||||||
|
const filePath = await save({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: "CSV",
|
||||||
|
extensions: ["csv"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaultPath: `fps_test_average_results_${new Date().toISOString().split("T")[0]}.csv`,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (filePath) {
|
||||||
|
await writeTextFile(filePath, csvContentWithBOM)
|
||||||
|
addToast({ title: "导出成功", color: "success" })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("导出CSV失败:", error)
|
||||||
|
addToast({
|
||||||
|
title: `导出失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
color: "danger",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
43
src/components/cstb/FpsTest/utils/fps-metrics.ts
Normal file
43
src/components/cstb/FpsTest/utils/fps-metrics.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// 从 VProf 报告中提取 avg 和 p1 值
|
||||||
|
export function extractFpsMetrics(result: string): { avg: number | null; p1: number | null } {
|
||||||
|
let avg: number | null = null
|
||||||
|
let p1: number | null = null
|
||||||
|
|
||||||
|
// 查找包含 avg 的行,支持多种格式:
|
||||||
|
// - "[VProf] FPS: Avg=239.5, P1=228.0" (等号格式)
|
||||||
|
// - "[VProf] avg: 123.45" (冒号格式)
|
||||||
|
// - "[VProf] avg 123.45" (空格格式)
|
||||||
|
const avgMatch = result.match(/avg[=:\s]+(\d+\.?\d*)/i)
|
||||||
|
if (avgMatch) {
|
||||||
|
avg = parseFloat(avgMatch[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找包含 p1 的行,支持多种格式:
|
||||||
|
// - "P1=228.0" (等号格式)
|
||||||
|
// - "p1: 98.76" (冒号格式)
|
||||||
|
// - "p1 98.76" (空格格式)
|
||||||
|
const p1Match = result.match(/p1[=:\s]+(\d+\.?\d*)/i)
|
||||||
|
if (p1Match) {
|
||||||
|
p1 = parseFloat(p1Match[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果找不到,尝试查找其他可能的格式
|
||||||
|
// 例如:查找包含 "fps" 和数字的行
|
||||||
|
if (!avg) {
|
||||||
|
const fpsMatch = result.match(/fps[=:\s]+(\d+\.?\d*)/i)
|
||||||
|
if (fpsMatch) {
|
||||||
|
avg = parseFloat(fpsMatch[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试查找 1% low 或类似的格式
|
||||||
|
if (!p1) {
|
||||||
|
const lowMatch = result.match(/(?:1%|1st|first).*?low[=:\s]+(\d+\.?\d*)/i)
|
||||||
|
if (lowMatch) {
|
||||||
|
p1 = parseFloat(lowMatch[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { avg, p1 }
|
||||||
|
}
|
||||||
|
|
||||||
66
src/components/cstb/FpsTest/utils/timestamp.ts
Normal file
66
src/components/cstb/FpsTest/utils/timestamp.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// 比较时间戳(格式:MM/DD HH:mm:ss)
|
||||||
|
// 返回 true 如果 timestamp1 晚于 timestamp2
|
||||||
|
export function compareTimestamps(timestamp1: string, timestamp2: string): boolean {
|
||||||
|
// 解析时间戳:MM/DD HH:mm:ss
|
||||||
|
const parseTimestamp = (ts: string) => {
|
||||||
|
const match = ts.match(/(\d{2})\/(\d{2})\s+(\d{2}):(\d{2}):(\d{2})/)
|
||||||
|
if (!match) return null
|
||||||
|
const [, month, day, hour, minute, second] = match.map(Number)
|
||||||
|
return { month, day, hour, minute, second }
|
||||||
|
}
|
||||||
|
|
||||||
|
const ts1 = parseTimestamp(timestamp1)
|
||||||
|
const ts2 = parseTimestamp(timestamp2)
|
||||||
|
|
||||||
|
if (!ts1 || !ts2) return false
|
||||||
|
|
||||||
|
// 使用当前年份作为基准
|
||||||
|
const now = new Date()
|
||||||
|
const currentYear = now.getFullYear()
|
||||||
|
|
||||||
|
// 创建日期对象,尝试当前年份
|
||||||
|
let date1 = new Date(currentYear, ts1.month - 1, ts1.day, ts1.hour, ts1.minute, ts1.second)
|
||||||
|
let date2 = new Date(currentYear, ts2.month - 1, ts2.day, ts2.hour, ts2.minute, ts2.second)
|
||||||
|
|
||||||
|
// 如果 date1 早于 date2,可能是跨年了(比如 date1 是 1月,date2 是 12月)
|
||||||
|
// 在这种情况下,给 date1 加一年
|
||||||
|
if (date1 < date2) {
|
||||||
|
// 检查是否可能是跨年(月份相差很大)
|
||||||
|
const monthDiff = (ts1.month - ts2.month + 12) % 12
|
||||||
|
if (monthDiff > 6) {
|
||||||
|
// 可能是跨年,给 date1 加一年
|
||||||
|
date1 = new Date(currentYear + 1, ts1.month - 1, ts1.day, ts1.hour, ts1.minute, ts1.second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return date1 > date2
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化当前时间为时间戳格式(MM/DD HH:mm:ss)
|
||||||
|
export function formatCurrentTimestamp(): string {
|
||||||
|
const now = new Date()
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, "0")
|
||||||
|
const day = String(now.getDate()).padStart(2, "0")
|
||||||
|
const hour = String(now.getHours()).padStart(2, "0")
|
||||||
|
const minute = String(now.getMinutes()).padStart(2, "0")
|
||||||
|
const second = String(now.getSeconds()).padStart(2, "0")
|
||||||
|
return `${month}/${day} ${hour}:${minute}:${second}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将时间戳转换为ISO格式
|
||||||
|
export function timestampToISO(timestamp: string): string {
|
||||||
|
const now = new Date()
|
||||||
|
const [monthDay, time] = timestamp.split(" ")
|
||||||
|
const [month, day] = monthDay.split("/")
|
||||||
|
const [hour, minute, second] = time.split(":")
|
||||||
|
const testDateTime = new Date(
|
||||||
|
now.getFullYear(),
|
||||||
|
parseInt(month) - 1,
|
||||||
|
parseInt(day),
|
||||||
|
parseInt(hour),
|
||||||
|
parseInt(minute),
|
||||||
|
parseInt(second)
|
||||||
|
)
|
||||||
|
return testDateTime.toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
47
src/components/cstb/FpsTest/utils/vprof-parser.ts
Normal file
47
src/components/cstb/FpsTest/utils/vprof-parser.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// 解析性能报告,提取时间戳和性能数据
|
||||||
|
export function parseVProfReport(rawReport: string): { timestamp: string; data: string } | null {
|
||||||
|
if (!rawReport) return null
|
||||||
|
|
||||||
|
const lines = rawReport.split("\n")
|
||||||
|
let timestamp = ""
|
||||||
|
let inPerformanceSection = false
|
||||||
|
const performanceLines: string[] = []
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// 提取时间戳:格式如 "11/05 01:51:27 [VProf] -- Performance report --"
|
||||||
|
const timestampMatch = line.match(
|
||||||
|
/(\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[VProf\]\s+--\s+Performance\s+report\s+--/
|
||||||
|
)
|
||||||
|
if (timestampMatch) {
|
||||||
|
timestamp = timestampMatch[1]
|
||||||
|
inPerformanceSection = true
|
||||||
|
// 也包含 Performance report 这一行,但移除时间戳
|
||||||
|
const lineWithoutTimestamp = line.trim().replace(/^\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2}\s+/, "")
|
||||||
|
performanceLines.push(lineWithoutTimestamp)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果在性能报告部分
|
||||||
|
if (inPerformanceSection) {
|
||||||
|
const trimmedLine = line.trim()
|
||||||
|
|
||||||
|
// 只收集包含 [VProf] 的行
|
||||||
|
if (trimmedLine.includes("[VProf]")) {
|
||||||
|
// 移除行首的时间戳(格式:MM/DD HH:mm:ss )
|
||||||
|
// 例如:"11/05 02:13:56 [VProf] ..." -> "[VProf] ..."
|
||||||
|
const lineWithoutTimestamp = trimmedLine.replace(/^\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2}\s+/, "")
|
||||||
|
performanceLines.push(lineWithoutTimestamp)
|
||||||
|
}
|
||||||
|
// 如果遇到空行且已经有数据,可能是报告结束,但不直接结束,因为可能还有更多数据
|
||||||
|
// 如果后续没有 [VProf] 行的数据,空行会被过滤掉
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (performanceLines.length === 0) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
timestamp,
|
||||||
|
data: performanceLines.join("\n").trim(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
import { useToolStore } from "@/store/tool"
|
import { LaunchOption as iLaunchOption, useToolStore } from "@/store/tool"
|
||||||
import { Plus, SettingConfig, Switch } from "@icon-park/react"
|
import { Badge, Close, Edit, Plus, Save, SettingConfig, Switch } from "@icon-park/react"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "../window/Card"
|
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "../window/Card"
|
||||||
import { ToolButton } from "../window/ToolButton"
|
import { ToolButton } from "../window/ToolButton"
|
||||||
import { Tooltip } from "@heroui/react"
|
import { Button, Input, Tooltip } from "@heroui/react"
|
||||||
|
import { remove } from "@tauri-apps/plugin-fs"
|
||||||
|
|
||||||
const LaunchOption = () => {
|
const LaunchOption = () => {
|
||||||
const tool = useToolStore()
|
const tool = useToolStore()
|
||||||
const [launchOpt, setLaunchOpt] = useState(tool.state.launchOptions[tool.state.launchIndex] || "")
|
const [launchOpt, setLaunchOpt] = useState(tool.state.launchOptions[tool.state.launchIndex] || "")
|
||||||
|
const [editMode, setEditMode] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLaunchOpt(tool.state.launchOptions[tool.state.launchIndex] || "")
|
setLaunchOpt(tool.state.launchOptions[tool.state.launchIndex] || "")
|
||||||
@@ -19,22 +21,71 @@ const LaunchOption = () => {
|
|||||||
<CardIcon>
|
<CardIcon>
|
||||||
<SettingConfig /> 启动选项
|
<SettingConfig /> 启动选项
|
||||||
</CardIcon>
|
</CardIcon>
|
||||||
<CardTool>
|
<CardTool className="">
|
||||||
{tool.state.launchOptions.map((option, index) => (
|
{tool.state.launchOptions.map((option, index) =>
|
||||||
<ToolButton key={index} onClick={() => tool.setLaunchIndex(index)}>
|
editMode ? (
|
||||||
{option.name || index + 1}
|
<Tooltip
|
||||||
</ToolButton>
|
key={index}
|
||||||
))}
|
radius="sm"
|
||||||
|
size="sm"
|
||||||
|
offset={-4}
|
||||||
|
crossOffset={18}
|
||||||
|
delay={300}
|
||||||
|
closeDelay={100}
|
||||||
|
placement="top-end"
|
||||||
|
color="foreground"
|
||||||
|
className="p-0 rounded-md bg-opacity-85"
|
||||||
|
content={
|
||||||
|
<button
|
||||||
|
className="p-1 text-small"
|
||||||
|
onClick={() => {
|
||||||
|
tool.removeLaunchOption(index)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Close size={12} />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={option.name}
|
||||||
|
onValueChange={(value: string) => {
|
||||||
|
tool.setLaunchOption({ ...option, name: value } as iLaunchOption, index)
|
||||||
|
}}
|
||||||
|
variant="bordered"
|
||||||
|
size="sm"
|
||||||
|
placeholder="名称"
|
||||||
|
fullWidth={false}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<ToolButton key={index} onClick={() => tool.setLaunchIndex(index)} selected={index === tool.state.launchIndex}>
|
||||||
|
{option.name || index + 1}
|
||||||
|
</ToolButton>
|
||||||
|
)
|
||||||
|
)}
|
||||||
<ToolButton onClick={() => tool.addLaunchOption({ option: "", name: "" })}>
|
<ToolButton onClick={() => tool.addLaunchOption({ option: "", name: "" })}>
|
||||||
<Plus />
|
<Plus />
|
||||||
添加
|
添加
|
||||||
</ToolButton>
|
</ToolButton>
|
||||||
<Tooltip content="功能测试中,尚未实装" showArrow={true} delay={300}>
|
{/* <Tooltip content="功能测试中,尚未实装" showArrow={true} delay={300}>
|
||||||
<ToolButton>
|
<ToolButton>
|
||||||
<Switch />
|
<Switch />
|
||||||
切换模式
|
切换模式
|
||||||
</ToolButton>
|
</ToolButton>
|
||||||
</Tooltip>
|
</Tooltip> */}
|
||||||
|
<ToolButton onClick={() => setEditMode(!editMode)}>
|
||||||
|
{editMode ? (
|
||||||
|
<>
|
||||||
|
<Save />
|
||||||
|
保存
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Edit />
|
||||||
|
修改
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ToolButton>
|
||||||
</CardTool>
|
</CardTool>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Key } from "@react-types/shared"
|
|||||||
import { useToolStore } from "@/store/tool"
|
import { useToolStore } from "@/store/tool"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
|
|
||||||
const PowerPlans = [
|
export const PowerPlans = [
|
||||||
{
|
{
|
||||||
id: "0",
|
id: "0",
|
||||||
title: "其他",
|
title: "其他",
|
||||||
@@ -77,6 +77,7 @@ const PowerPlan = () => {
|
|||||||
radius="full"
|
radius="full"
|
||||||
fullWidth
|
fullWidth
|
||||||
defaultSelectedKey="0"
|
defaultSelectedKey="0"
|
||||||
|
className="gap-0"
|
||||||
>
|
>
|
||||||
{PowerPlans.slice(1).map((item) => (
|
{PowerPlans.slice(1).map((item) => (
|
||||||
<Tab key={item.id} title={item.title}></Tab>
|
<Tab key={item.id} title={item.title}></Tab>
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ export function Prepare() {
|
|||||||
const init = async () => {
|
const init = async () => {
|
||||||
await steam.store.start()
|
await steam.store.start()
|
||||||
await app.store.start()
|
await app.store.start()
|
||||||
if (!app.state.inited) await autoGetPaths(false)
|
|
||||||
setInited(true)
|
setInited(true)
|
||||||
}
|
}
|
||||||
void init()
|
void init()
|
||||||
@@ -66,6 +65,7 @@ export function Prepare() {
|
|||||||
setLoading(false)
|
setLoading(false)
|
||||||
}, 1200)
|
}, 1200)
|
||||||
}
|
}
|
||||||
|
// router.push("/home")
|
||||||
}, [inited])
|
}, [inited])
|
||||||
|
|
||||||
const handleSelectSteamDir = async () => {
|
const handleSelectSteamDir = async () => {
|
||||||
|
|||||||
@@ -1,13 +1,41 @@
|
|||||||
import { Refresh, User } from "@icon-park/react"
|
import { Refresh, User, FolderFocusOne, Login, Check, List, GridFour } from "@icon-park/react"
|
||||||
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "../window/Card"
|
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "../window/Card"
|
||||||
import { addToast, Button, Chip } from "@heroui/react"
|
import { addToast, Button, Chip, Tabs, Tab, Tooltip } from "@heroui/react"
|
||||||
import { useSteamStore } from "@/store/steam"
|
import { useSteamStore } from "@/store/steam"
|
||||||
|
import { useAppStore } from "@/store/app"
|
||||||
import { ToolButton } from "../window/ToolButton"
|
import { ToolButton } from "../window/ToolButton"
|
||||||
import { useAutoAnimate } from "@formkit/auto-animate/react"
|
import { useAutoAnimate } from "@formkit/auto-animate/react"
|
||||||
|
import { invoke } from "@tauri-apps/api/core"
|
||||||
|
import { writeText } from "@tauri-apps/plugin-clipboard-manager"
|
||||||
|
import path from "path"
|
||||||
|
import { useState, useMemo } from "react"
|
||||||
|
import type { SteamUser } from "@/types/steam"
|
||||||
|
|
||||||
|
type ViewMode = "card" | "list" | "list-large"
|
||||||
|
|
||||||
const SteamUsers = ({ className }: { className?: string }) => {
|
const SteamUsers = ({ className }: { className?: string }) => {
|
||||||
const steam = useSteamStore()
|
const steam = useSteamStore()
|
||||||
|
const app = useAppStore()
|
||||||
const [parent /* , enableAnimations */] = useAutoAnimate(/* optional config */)
|
const [parent /* , enableAnimations */] = useAutoAnimate(/* optional config */)
|
||||||
|
const viewMode = app.state.steamUsersViewMode as ViewMode
|
||||||
|
const [mockUsers, setMockUsers] = useState<SteamUser[] | null>(null)
|
||||||
|
|
||||||
|
// 生成模拟用户数据
|
||||||
|
const generateMockUsers = (): SteamUser[] => {
|
||||||
|
return Array.from({ length: 200 }, (_, i) => ({
|
||||||
|
steam_id64: BigInt(76561198000000000 + i),
|
||||||
|
steam_id32: 1000000 + i,
|
||||||
|
account_name: `mockuser${i + 1}`,
|
||||||
|
persona_name: `模拟用户 ${i + 1}`,
|
||||||
|
recent: i < 3 ? 1 : 0,
|
||||||
|
avatar: "",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用模拟数据或真实数据
|
||||||
|
const displayUsers = useMemo(() => {
|
||||||
|
return mockUsers || steam.state.users
|
||||||
|
}, [mockUsers, steam.state.users])
|
||||||
|
|
||||||
const getUsers = async (toast?: boolean) => {
|
const getUsers = async (toast?: boolean) => {
|
||||||
if (!steam.state.steamDirValid) {
|
if (!steam.state.steamDirValid) {
|
||||||
@@ -19,62 +47,382 @@ const SteamUsers = ({ className }: { className?: string }) => {
|
|||||||
if (toast) addToast({ title: `已获取Steam用户` })
|
if (toast) addToast({ title: `已获取Steam用户` })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleMockToggle = () => {
|
||||||
|
if (mockUsers) {
|
||||||
|
setMockUsers(null)
|
||||||
|
addToast({ title: "已切换回真实数据" })
|
||||||
|
} else {
|
||||||
|
setMockUsers(generateMockUsers())
|
||||||
|
addToast({ title: "已切换到模拟数据(200个用户)" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制到剪贴板的辅助函数
|
||||||
|
const handleCopy = async (text: string, label: string) => {
|
||||||
|
try {
|
||||||
|
await writeText(text)
|
||||||
|
addToast({ title: `已复制${label}` })
|
||||||
|
} catch (error) {
|
||||||
|
addToast({ title: "复制失败", color: "danger" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染用户项 - 列表-大样式(当前样式)
|
||||||
|
const renderListLargeItem = (user: SteamUser, id: number, isMock: boolean) => (
|
||||||
|
<li
|
||||||
|
key={`${user.account_name}-${id}`}
|
||||||
|
className="flex gap-2 transition border rounded-lg bg-white/70 border-zinc-200/50 dark:border-white/5 dark:bg-zinc-900/50"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={user.avatar ? `data:image/png;base64,${user.avatar}` : "/logo_square.png"}
|
||||||
|
alt="avatar"
|
||||||
|
className="w-20 h-20 rounded-l-lg"
|
||||||
|
draggable="false"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col grow justify-center gap-2 p-0.5">
|
||||||
|
<h3
|
||||||
|
className="text-xl font-semibold cursor-pointer hover:underline"
|
||||||
|
onClick={() => handleCopy(user.persona_name, "用户名")}
|
||||||
|
>
|
||||||
|
{user.persona_name}
|
||||||
|
</h3>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Chip
|
||||||
|
size="sm"
|
||||||
|
className="cursor-pointer bg-zinc-200 dark:bg-zinc-800 hover:opacity-80"
|
||||||
|
onClick={() => handleCopy(user.account_name, "账号名")}
|
||||||
|
>
|
||||||
|
{user.account_name}
|
||||||
|
</Chip>
|
||||||
|
<Chip
|
||||||
|
size="sm"
|
||||||
|
className="cursor-pointer bg-zinc-200 dark:bg-zinc-800 hover:opacity-80"
|
||||||
|
onClick={() => handleCopy(user.steam_id32.toString(), "Steam32位ID")}
|
||||||
|
>
|
||||||
|
{user.steam_id32}
|
||||||
|
</Chip>
|
||||||
|
<Chip
|
||||||
|
size="sm"
|
||||||
|
className="cursor-pointer bg-zinc-200 dark:bg-zinc-800 hover:opacity-80"
|
||||||
|
onClick={() => handleCopy(user.steam_id64.toString(), "Steam64位ID")}
|
||||||
|
>
|
||||||
|
{user.steam_id64.toString()}
|
||||||
|
</Chip>
|
||||||
|
{user.recent > 0 && (
|
||||||
|
<Chip size="sm" color="primary">
|
||||||
|
最近登录
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end gap-2 p-2 shrink-0">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onPress={async () => {
|
||||||
|
if (!steam.state.steamDirValid) {
|
||||||
|
addToast({ title: "Steam路径不可用", color: "warning" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await invoke("open_path", {
|
||||||
|
path: path.resolve(
|
||||||
|
steam.state.steamDir,
|
||||||
|
"userdata",
|
||||||
|
user.steam_id32.toString(),
|
||||||
|
"730",
|
||||||
|
"local",
|
||||||
|
"cfg"
|
||||||
|
),
|
||||||
|
})
|
||||||
|
addToast({ title: "个人CFG" })
|
||||||
|
}}
|
||||||
|
className="gap-1"
|
||||||
|
>
|
||||||
|
个人CFG
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onPress={() => steam.switchLoginUser(id)}
|
||||||
|
className="gap-1"
|
||||||
|
isDisabled={isMock}
|
||||||
|
>
|
||||||
|
切换登录
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onPress={() => steam.selectUser(id)}
|
||||||
|
className="gap-1"
|
||||||
|
isDisabled={isMock}
|
||||||
|
>
|
||||||
|
<Check size={14} />
|
||||||
|
选择
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
|
||||||
|
// 渲染用户项 - 列表样式(缩小版)
|
||||||
|
const renderListItem = (user: SteamUser, id: number, isMock: boolean) => (
|
||||||
|
<li
|
||||||
|
key={`${user.account_name}-${id}`}
|
||||||
|
className="flex gap-2 transition border rounded-lg bg-white/70 border-zinc-200/50 dark:border-white/5 dark:bg-zinc-900/50"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={user.avatar ? `data:image/png;base64,${user.avatar}` : "/logo_square.png"}
|
||||||
|
alt="avatar"
|
||||||
|
className="rounded-l-lg w-14 h-14"
|
||||||
|
draggable="false"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col grow justify-center gap-1.5 p-1.5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3
|
||||||
|
className="text-base font-semibold cursor-pointer hover:underline"
|
||||||
|
onClick={() => handleCopy(user.persona_name, "用户名")}
|
||||||
|
>
|
||||||
|
{user.persona_name}
|
||||||
|
</h3>
|
||||||
|
{user.recent > 0 && (
|
||||||
|
<Chip size="sm" color="primary" className="text-[10px] px-1 h-5">
|
||||||
|
最近登录
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1.5 flex-wrap items-center">
|
||||||
|
<span
|
||||||
|
className="text-xs cursor-pointer text-zinc-600 dark:text-zinc-400 hover:underline hover:decoration-zinc-300 dark:hover:decoration-zinc-600"
|
||||||
|
onClick={() => handleCopy(user.account_name, "账号名")}
|
||||||
|
>
|
||||||
|
{user.account_name}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="text-xs cursor-pointer text-zinc-600 dark:text-zinc-400 hover:underline hover:decoration-zinc-300 dark:hover:decoration-zinc-600"
|
||||||
|
onClick={() => handleCopy(user.steam_id32.toString(), "Steam32位ID")}
|
||||||
|
>
|
||||||
|
{user.steam_id32}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="text-xs cursor-pointer text-zinc-600 dark:text-zinc-400 hover:underline hover:decoration-zinc-300 dark:hover:decoration-zinc-600"
|
||||||
|
onClick={() => handleCopy(user.steam_id64.toString(), "Steam64位ID")}
|
||||||
|
>
|
||||||
|
{user.steam_id64.toString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-1.5 mr-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
radius="full"
|
||||||
|
onPress={async () => {
|
||||||
|
if (!steam.state.steamDirValid) {
|
||||||
|
addToast({ title: "Steam路径不可用", color: "warning" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await invoke("open_path", {
|
||||||
|
path: path.resolve(
|
||||||
|
steam.state.steamDir,
|
||||||
|
"userdata",
|
||||||
|
user.steam_id32.toString(),
|
||||||
|
"730",
|
||||||
|
"local",
|
||||||
|
"cfg"
|
||||||
|
),
|
||||||
|
})
|
||||||
|
addToast({ title: "个人CFG" })
|
||||||
|
}}
|
||||||
|
className="gap-1 px-3 h-7"
|
||||||
|
>
|
||||||
|
个人CFG
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
radius="full"
|
||||||
|
onPress={() => steam.switchLoginUser(id)}
|
||||||
|
className="gap-1 px-3 h-7"
|
||||||
|
isDisabled={isMock}
|
||||||
|
>
|
||||||
|
切换登录
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
radius="full"
|
||||||
|
onPress={() => steam.selectUser(id)}
|
||||||
|
className="gap-1 px-3 h-7"
|
||||||
|
isDisabled={isMock}
|
||||||
|
>
|
||||||
|
<Check size={12} />
|
||||||
|
选择
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
|
||||||
|
// 渲染用户项 - 卡片样式(grid布局)
|
||||||
|
const renderCardItem = (user: SteamUser, id: number, isMock: boolean) => (
|
||||||
|
<li
|
||||||
|
key={`${user.account_name}-${id}`}
|
||||||
|
className="flex flex-col gap-2 p-3 transition border rounded-lg bg-white/70 border-zinc-200/50 dark:border-white/5 dark:bg-zinc-900/50 h-fit"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<img
|
||||||
|
src={user.avatar ? `data:image/png;base64,${user.avatar}` : "/logo_square.png"}
|
||||||
|
alt="avatar"
|
||||||
|
className="w-12 h-12 rounded-lg shrink-0"
|
||||||
|
draggable="false"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col min-w-0 grow">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3
|
||||||
|
className="text-sm font-semibold leading-relaxed truncate cursor-pointer hover:underline"
|
||||||
|
onClick={() => handleCopy(user.persona_name, "用户名")}
|
||||||
|
>
|
||||||
|
{user.persona_name}
|
||||||
|
</h3>
|
||||||
|
{user.recent > 0 && (
|
||||||
|
<Chip size="sm" color="primary" className="text-[10px] px-1 h-5 shrink-0">
|
||||||
|
最近登录
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
className="text-xs leading-normal truncate cursor-pointer text-zinc-500 hover:underline"
|
||||||
|
onClick={() => handleCopy(user.account_name, "账号名")}
|
||||||
|
>
|
||||||
|
{user.account_name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1.5 flex-wrap">
|
||||||
|
<Tooltip content="Steam32位id" showArrow={true} delay={300}>
|
||||||
|
<span
|
||||||
|
className="text-xs cursor-pointer text-zinc-500 dark:text-zinc-400 hover:underline"
|
||||||
|
onClick={() => handleCopy(user.steam_id32.toString(), "Steam32位ID")}
|
||||||
|
>
|
||||||
|
{user.steam_id32}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Steam64位id" showArrow={true} delay={300}>
|
||||||
|
<span
|
||||||
|
className="text-xs cursor-pointer text-zinc-500 dark:text-zinc-400 hover:underline"
|
||||||
|
onClick={() => handleCopy(user.steam_id64.toString(), "Steam64位ID")}
|
||||||
|
>
|
||||||
|
{user.steam_id64.toString()}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1.5 mt-auto">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
radius="full"
|
||||||
|
onPress={async () => {
|
||||||
|
if (!steam.state.steamDirValid) {
|
||||||
|
addToast({ title: "Steam路径不可用", color: "warning" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await invoke("open_path", {
|
||||||
|
path: path.resolve(
|
||||||
|
steam.state.steamDir,
|
||||||
|
"userdata",
|
||||||
|
user.steam_id32.toString(),
|
||||||
|
"730",
|
||||||
|
"local",
|
||||||
|
"cfg"
|
||||||
|
),
|
||||||
|
})
|
||||||
|
addToast({ title: "个人CFG" })
|
||||||
|
}}
|
||||||
|
className="flex h-7"
|
||||||
|
>
|
||||||
|
个人CFG
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
radius="full"
|
||||||
|
onPress={() => steam.switchLoginUser(id)}
|
||||||
|
className="flex h-7"
|
||||||
|
isDisabled={isMock}
|
||||||
|
>
|
||||||
|
切换登录
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
radius="full"
|
||||||
|
onPress={() => steam.selectUser(id)}
|
||||||
|
className="flex h-7"
|
||||||
|
isDisabled={isMock}
|
||||||
|
>
|
||||||
|
<Check size={10} />
|
||||||
|
选择
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
|
||||||
|
// 根据视图模式渲染用户项
|
||||||
|
const renderUserItem = (user: SteamUser, id: number) => {
|
||||||
|
const isMock = !!mockUsers
|
||||||
|
const realIndex = mockUsers
|
||||||
|
? -1
|
||||||
|
: steam.state.users.findIndex((u) => u.account_name === user.account_name)
|
||||||
|
const index = realIndex >= 0 ? realIndex : id
|
||||||
|
|
||||||
|
switch (viewMode) {
|
||||||
|
case "card":
|
||||||
|
return renderCardItem(user, index, isMock)
|
||||||
|
case "list":
|
||||||
|
return renderListItem(user, index, isMock)
|
||||||
|
case "list-large":
|
||||||
|
default:
|
||||||
|
return renderListLargeItem(user, index, isMock)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card /* className={cn("max-w-96", className)} */>
|
<Card className="flex flex-col h-full overflow-hidden">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardIcon>
|
<CardIcon>
|
||||||
<User /> Steam用户
|
<User /> Steam用户
|
||||||
</CardIcon>
|
</CardIcon>
|
||||||
<CardTool>
|
<CardTool>
|
||||||
|
<Tooltip content="切换样式" showArrow={true} delay={300}>
|
||||||
|
<Tabs
|
||||||
|
selectedKey={viewMode}
|
||||||
|
onSelectionChange={(key) => app.setSteamUsersViewMode(key as ViewMode)}
|
||||||
|
size="sm"
|
||||||
|
radius="full"
|
||||||
|
classNames={{
|
||||||
|
base: "min-w-0",
|
||||||
|
tabList: "gap-0 p-0",
|
||||||
|
tab: "min-w-0 px-2",
|
||||||
|
tabContent: "text-xs",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tab key="card" title={<GridFour size={14} />} />
|
||||||
|
<Tab key="list" title={<List size={14} />} />
|
||||||
|
<Tab key="list-large" title={<List size={16} />} />
|
||||||
|
</Tabs>
|
||||||
|
</Tooltip>
|
||||||
|
{/* <ToolButton onClick={handleMockToggle}>{mockUsers ? "真实" : "模拟"}</ToolButton> */}
|
||||||
<ToolButton onClick={() => getUsers(true)}>
|
<ToolButton onClick={() => getUsers(true)}>
|
||||||
<Refresh />
|
<Refresh />
|
||||||
刷新
|
刷新
|
||||||
</ToolButton>
|
</ToolButton>
|
||||||
</CardTool>
|
</CardTool>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody className="flex-1 min-h-0 overflow-hidden">
|
||||||
<ul className="flex flex-col gap-3 mt-1" ref={parent}>
|
{viewMode === "card" ? (
|
||||||
{steam.state.users.map((user, id) => (
|
<ul
|
||||||
<li
|
className="grid grid-cols-2 gap-3 mt-1 overflow-y-auto rounded-lg auto-rows-max md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 pb-11 hide-scrollbar"
|
||||||
key={user.account_name}
|
ref={parent}
|
||||||
className="flex gap-2 transition rounded-lg bg-zinc-50 dark:bg-zinc-900"
|
>
|
||||||
>
|
{displayUsers.map((user, id) => renderUserItem(user, id))}
|
||||||
<img
|
</ul>
|
||||||
src={user.avatar ? `data:image/png;base64,${user.avatar}` : "/logo_square.png"}
|
) : (
|
||||||
alt="avatar"
|
<ul
|
||||||
className="w-20 h-20 rounded-l-lg"
|
className="flex flex-col h-full gap-3 mt-1 overflow-y-auto rounded-lg pb-11 hide-scrollbar"
|
||||||
draggable="false"
|
ref={parent}
|
||||||
/>
|
>
|
||||||
<div className="flex flex-col flex-grow justify-center gap-2 p-0.5">
|
{displayUsers.map((user, id) => renderUserItem(user, id))}
|
||||||
<h3 className="text-xl font-semibold">{user.persona_name}</h3>
|
</ul>
|
||||||
<div className="flex gap-2">
|
)}
|
||||||
<Chip size="sm" className="bg-zinc-200 dark:bg-zinc-800">
|
|
||||||
{user.account_name}
|
|
||||||
</Chip>
|
|
||||||
<Chip size="sm" className="bg-zinc-200 dark:bg-zinc-800">
|
|
||||||
{user.steam_id32}
|
|
||||||
</Chip>
|
|
||||||
<Chip size="sm" className="bg-zinc-200 dark:bg-zinc-800">
|
|
||||||
{user.steam_id64.toString()}
|
|
||||||
</Chip>
|
|
||||||
{user.recent > 0 && (
|
|
||||||
<Chip size="sm" color="primary">
|
|
||||||
最近登录
|
|
||||||
</Chip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-end gap-2 p-2">
|
|
||||||
<Button size="sm" onPress={() => steam.switchLoginUser(id)}>
|
|
||||||
切换登录
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" onPress={() => steam.selectUser(id)}>
|
|
||||||
选择
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
339
src/components/cstb/UpdateChecker.tsx
Normal file
339
src/components/cstb/UpdateChecker.tsx
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
useDisclosure,
|
||||||
|
} from "@heroui/react"
|
||||||
|
import { Download, Refresh, FileText, Close, Check } from "@icon-park/react"
|
||||||
|
import { invoke } from "@tauri-apps/api/core"
|
||||||
|
import { listen } from "@tauri-apps/api/event"
|
||||||
|
import { relaunch } from "@tauri-apps/plugin-process"
|
||||||
|
import { addToast } from "@heroui/react"
|
||||||
|
import { useAppStore, type UpdateInfo } from "@/store/app"
|
||||||
|
import { MarkdownRender } from "@/components/markdown"
|
||||||
|
|
||||||
|
interface UpdateCheckerProps {
|
||||||
|
customEndpoint?: string
|
||||||
|
includePrerelease?: boolean
|
||||||
|
useCdn?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UpdateChecker({
|
||||||
|
customEndpoint,
|
||||||
|
includePrerelease = false,
|
||||||
|
useCdn = true,
|
||||||
|
}: UpdateCheckerProps) {
|
||||||
|
const app = useAppStore()
|
||||||
|
const [checking, setChecking] = useState(false)
|
||||||
|
// 从 store 读取状态
|
||||||
|
const updateInfo = app.state.updateInfo
|
||||||
|
const downloading = app.state.downloading
|
||||||
|
const downloadProgress = app.state.downloadProgress
|
||||||
|
const downloadCompleted = app.state.downloadCompleted
|
||||||
|
const {
|
||||||
|
isOpen: isChangelogOpen,
|
||||||
|
onOpen: onChangelogOpen,
|
||||||
|
onOpenChange: onChangelogOpenChange,
|
||||||
|
} = useDisclosure()
|
||||||
|
|
||||||
|
// 监听下载进度事件
|
||||||
|
useEffect(() => {
|
||||||
|
const unlisten = listen<number>("update-download-progress", (event) => {
|
||||||
|
const progress = event.payload
|
||||||
|
app.setDownloadProgress(progress)
|
||||||
|
|
||||||
|
// 如果进度达到 100%,标记下载完成
|
||||||
|
if (progress === 100) {
|
||||||
|
app.setDownloading(false)
|
||||||
|
app.setDownloadCompleted(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unlisten.then((fn) => fn())
|
||||||
|
}
|
||||||
|
}, [app.setDownloadProgress, app.setDownloading, app.setDownloadCompleted])
|
||||||
|
|
||||||
|
// 检查更新
|
||||||
|
const handleCheckUpdate = async () => {
|
||||||
|
setChecking(true)
|
||||||
|
app.setUpdateInfo(null)
|
||||||
|
app.setDownloadProgress(0)
|
||||||
|
app.setDownloading(false)
|
||||||
|
app.setDownloadCompleted(false)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 根据是否包含测试版来选择不同的 endpoint
|
||||||
|
// 如果提供了 customEndpoint,优先使用;否则根据 includePrerelease 动态选择
|
||||||
|
let endpoint: string | null = null
|
||||||
|
if (customEndpoint) {
|
||||||
|
// 如果提供了 customEndpoint,仍然使用它(用于特殊场景)
|
||||||
|
endpoint = customEndpoint
|
||||||
|
} else {
|
||||||
|
// 根据 includePrerelease 选择对应的 endpoint
|
||||||
|
if (includePrerelease) {
|
||||||
|
endpoint = "https://gh-info.okk.cool/repos/plsgo/cstb/releases/latest/pre/tauri"
|
||||||
|
} else {
|
||||||
|
endpoint = "https://gh-info.okk.cool/repos/plsgo/cstb/releases/latest/tauri"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await invoke<UpdateInfo | null>("check_app_update", {
|
||||||
|
endpoint: endpoint,
|
||||||
|
includePrerelease: includePrerelease,
|
||||||
|
useCdn: useCdn,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
app.setUpdateInfo(result)
|
||||||
|
// 更新 store 中的更新状态和最新版本号
|
||||||
|
app.setHasUpdate(true)
|
||||||
|
app.setLatestVersion(result.version)
|
||||||
|
addToast({
|
||||||
|
title: "发现新版本",
|
||||||
|
description: `版本 ${result.version} 可用`,
|
||||||
|
color: "success",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 没有更新,更新 store 状态
|
||||||
|
app.setHasUpdate(false)
|
||||||
|
app.setLatestVersion("")
|
||||||
|
app.setUpdateInfo(null)
|
||||||
|
addToast({
|
||||||
|
title: "已是最新版本",
|
||||||
|
color: "default",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
addToast({
|
||||||
|
title: "检查更新失败",
|
||||||
|
description: String(error),
|
||||||
|
color: "danger",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setChecking(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载更新
|
||||||
|
const handleDownloadUpdate = async () => {
|
||||||
|
if (!updateInfo) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app.setDownloading(true)
|
||||||
|
app.setDownloadProgress(0)
|
||||||
|
app.setDownloadCompleted(false)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用官方 updater 插件,传递 useCdn 参数
|
||||||
|
await invoke("download_app_update", { useCdn: useCdn })
|
||||||
|
|
||||||
|
// 下载完成,标记状态
|
||||||
|
app.setDownloadProgress(100)
|
||||||
|
app.setDownloading(false)
|
||||||
|
app.setDownloadCompleted(true)
|
||||||
|
|
||||||
|
addToast({
|
||||||
|
title: "下载完成",
|
||||||
|
description: "可以点击安装按钮进行安装",
|
||||||
|
color: "success",
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = String(error)
|
||||||
|
if (errorMsg.includes("取消")) {
|
||||||
|
addToast({
|
||||||
|
title: "下载已取消",
|
||||||
|
color: "default",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
addToast({
|
||||||
|
title: "下载失败",
|
||||||
|
description: errorMsg,
|
||||||
|
color: "danger",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
app.setDownloadProgress(0)
|
||||||
|
app.setDownloading(false)
|
||||||
|
app.setDownloadCompleted(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消下载
|
||||||
|
const handleCancelDownload = async () => {
|
||||||
|
try {
|
||||||
|
await invoke("cancel_download_update")
|
||||||
|
app.setDownloading(false)
|
||||||
|
app.setDownloadProgress(0)
|
||||||
|
app.setDownloadCompleted(false)
|
||||||
|
addToast({
|
||||||
|
title: "已取消下载",
|
||||||
|
color: "default",
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
// 静默处理错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安装更新
|
||||||
|
const handleInstallUpdate = async () => {
|
||||||
|
if (!downloadCompleted) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
addToast({
|
||||||
|
title: "安装已启动",
|
||||||
|
description: "应用将自动重启",
|
||||||
|
color: "success",
|
||||||
|
})
|
||||||
|
|
||||||
|
// 使用官方 updater 插件,不需要传递 installerPath
|
||||||
|
// 在 Windows 上,应用会自动退出以安装更新
|
||||||
|
await invoke("install_app_update")
|
||||||
|
|
||||||
|
// 在非 Windows 平台上,可能需要手动重启
|
||||||
|
// Windows 上会自动退出,所以这里不需要 relaunch
|
||||||
|
// 等待一小段时间确保安装程序启动
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
} catch (error) {
|
||||||
|
addToast({
|
||||||
|
title: "安装失败",
|
||||||
|
description: String(error),
|
||||||
|
color: "danger",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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}
|
||||||
|
className="w-fit"
|
||||||
|
>
|
||||||
|
{checking ? "检查中..." : "检查更新"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{updateInfo && (
|
||||||
|
<>
|
||||||
|
{!downloading && !downloadCompleted && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
startContent={<Download />}
|
||||||
|
onPress={handleDownloadUpdate}
|
||||||
|
className="w-fit"
|
||||||
|
>
|
||||||
|
下载更新
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{downloading && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
color="danger"
|
||||||
|
variant="flat"
|
||||||
|
startContent={<Close />}
|
||||||
|
onPress={handleCancelDownload}
|
||||||
|
className="w-fit"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{downloadCompleted && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
onPress={handleInstallUpdate}
|
||||||
|
className="w-fit"
|
||||||
|
>
|
||||||
|
安装更新
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
color="default"
|
||||||
|
variant="flat"
|
||||||
|
startContent={<FileText />}
|
||||||
|
onPress={onChangelogOpen}
|
||||||
|
className="w-fit"
|
||||||
|
>
|
||||||
|
更新日志
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{(downloading || downloadProgress > 0 || downloadCompleted) && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{downloadCompleted ? (
|
||||||
|
<>
|
||||||
|
<Check className="text-green-500 dark:text-green-400" size={14} />
|
||||||
|
<span className="text-xs text-green-500 dark:text-green-400">下载完成</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CircularProgress
|
||||||
|
aria-label="下载进度"
|
||||||
|
value={downloadProgress}
|
||||||
|
color="primary"
|
||||||
|
size="sm"
|
||||||
|
showValueLabel={true}
|
||||||
|
classNames={{
|
||||||
|
value: "text-xs text-zinc-500 dark:text-zinc-400",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* <span className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||||
|
{downloadProgress}%
|
||||||
|
</span> */}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 更新日志对话框 */}
|
||||||
|
<Modal isOpen={isChangelogOpen} onOpenChange={onChangelogOpenChange} size="lg">
|
||||||
|
<ModalContent>
|
||||||
|
{(onClose) => (
|
||||||
|
<>
|
||||||
|
<ModalHeader className="flex flex-col gap-1">
|
||||||
|
<span>更新日志 v{updateInfo?.version}</span>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
{updateInfo?.notes ? (
|
||||||
|
<div className="text-sm text-zinc-600 dark:text-zinc-400">
|
||||||
|
<MarkdownRender>{updateInfo.notes}</MarkdownRender>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-zinc-500">暂无更新日志</p>
|
||||||
|
)}
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button color="primary" variant="flat" onPress={onClose}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,178 +1,623 @@
|
|||||||
import { CloseSmall, Down, Edit, Plus, SettingConfig, Up } from "@icon-park/react"
|
import { CloseSmall, Down, Edit, Plus, SettingConfig, Up } from "@icon-park/react"
|
||||||
import { useState } from "react"
|
import { useEffect, useState, useCallback, useRef } from "react"
|
||||||
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "../window/Card"
|
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "../window/Card"
|
||||||
import { ToolButton } from "../window/ToolButton"
|
import { ToolButton } from "../window/ToolButton"
|
||||||
import { addToast, NumberInput, Tab, Tabs, Tooltip } from "@heroui/react"
|
import {
|
||||||
|
addToast,
|
||||||
|
Input,
|
||||||
|
Tab,
|
||||||
|
Tabs,
|
||||||
|
Tooltip,
|
||||||
|
Chip,
|
||||||
|
Dropdown,
|
||||||
|
DropdownTrigger,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownItem,
|
||||||
|
Button,
|
||||||
|
} from "@heroui/react"
|
||||||
import { motion } from "framer-motion"
|
import { motion } from "framer-motion"
|
||||||
import { useToolStore } from "@/store/tool"
|
import { useToolStore, VideoSetting as VideoConfig, VideoSettingTemplate } from "@/store/tool"
|
||||||
|
import { useSteamStore } from "@/store/steam"
|
||||||
|
import { useDebounce, useDebounceFn, useThrottleFn } from "ahooks"
|
||||||
|
import { invoke } from "@tauri-apps/api/core"
|
||||||
|
import { listen } from "@tauri-apps/api/event"
|
||||||
|
import { useGlobalGameMonitor } from "@/hooks/useGlobalGameMonitor"
|
||||||
|
import { PRESET_RESOLUTIONS } from "./FpsTest/constants"
|
||||||
|
|
||||||
const VideoSetting = () => {
|
const VideoSetting = () => {
|
||||||
const [hide, setHide] = useState(false)
|
const [hide, setHide] = useState(false)
|
||||||
const [edit, setEdit] = useState(false)
|
const [edit, setEdit] = useState(false)
|
||||||
const tool = useToolStore()
|
const tool = useToolStore()
|
||||||
// const [launchOpt, setLaunchOpt] = useState(tool.state.VideoSettings[tool.state.launchIndex] || "")
|
const steam = useSteamStore()
|
||||||
|
// 使用全局游戏状态监控,避免重复检测
|
||||||
|
const { isGameRunning, checkGameRunning } = useGlobalGameMonitor()
|
||||||
|
// 使用 ref 存储 edit 的最新值,供 throttle 回调使用
|
||||||
|
const editRef = useRef(edit)
|
||||||
|
useEffect(() => {
|
||||||
|
editRef.current = edit
|
||||||
|
}, [edit])
|
||||||
|
|
||||||
// useEffect(() => {
|
// 防抖的读取函数
|
||||||
// setLaunchOpt(tool.state.VideoSettings[tool.state.launchIndex] || "")
|
const { run: debouncedGetVideoConfig } = useDebounceFn(
|
||||||
// }, [tool.state.launchIndex, tool.state.VideoSettings])
|
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: false, trailing: true }
|
||||||
|
)
|
||||||
|
const videoSettings = (video: VideoConfig) => {
|
||||||
|
// 判断当前全屏模式
|
||||||
|
const getFullscreenMode = () => {
|
||||||
|
if (video.fullscreen === "1") {
|
||||||
|
return "全屏"
|
||||||
|
} else if (video.coop_fullscreen === "1") {
|
||||||
|
return "全屏窗口化"
|
||||||
|
} else {
|
||||||
|
return "窗口"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 设置对应关系
|
return [
|
||||||
// TODO Value通过实际数值映射
|
{
|
||||||
const videoSettings = [
|
type: "fullscreen",
|
||||||
{ type: "", title: "全屏", value: "全屏", options: ["窗口", "全屏"] },
|
title: "显示模式",
|
||||||
{ type: "", title: "垂直同步", value: "关闭", options: ["关闭", "开启"] },
|
value: getFullscreenMode(),
|
||||||
{ type: "", title: "低延迟模式", value: "关闭", options: ["关闭", "开启"] },
|
options: ["窗口", "全屏", "全屏窗口化"],
|
||||||
{ type: "", title: "增强角色对比度", value: "禁用", options: ["禁用", "启用"] },
|
mapping: (value: string) => {
|
||||||
{ type: "", title: "CMAA2抗锯齿", value: "关闭", options: ["关闭", "开启"] },
|
// 返回一个对象,包含需要同时更新的字段
|
||||||
{
|
return {
|
||||||
type: "",
|
窗口: { fullscreen: "0", coop_fullscreen: "0" },
|
||||||
title: "多重采样抗锯齿",
|
全屏: { fullscreen: "1", coop_fullscreen: "0" },
|
||||||
value: "2X MSAA",
|
全屏窗口化: { fullscreen: "0", coop_fullscreen: "1" },
|
||||||
options: ["无", "2X MSAA", "4X MSAA", "8X MSAA"],
|
}[value] || { fullscreen: "0", coop_fullscreen: "0" }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "mat_vsync",
|
||||||
|
title: "垂直同步",
|
||||||
|
value: video.mat_vsync === "1" ? "开启" : "关闭",
|
||||||
|
options: ["关闭", "开启"],
|
||||||
|
mapping: (value: string) => {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
关闭: "0",
|
||||||
|
开启: "1",
|
||||||
|
}[value] || "0"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "r_low_latency",
|
||||||
|
title: "低延迟模式",
|
||||||
|
value: video.r_low_latency === "1" ? "开启" : "关闭",
|
||||||
|
options: ["关闭", "开启"],
|
||||||
|
mapping: (value: string) => {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
关闭: "0",
|
||||||
|
开启: "1",
|
||||||
|
}[value] || "0"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// TODO: 改选项不在 cs2_video.txt 中
|
||||||
|
// {
|
||||||
|
// type: "r_player_visible_mode",
|
||||||
|
// title: "增强角色对比度",
|
||||||
|
// value: video.r_csgo_cmaa_enable === "1" ? "启用" : "禁用",
|
||||||
|
// options: ["禁用", "启用"],
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
type: "r_csgo_cmaa_enable",
|
||||||
|
title: "CMAA2抗锯齿",
|
||||||
|
value: video.r_csgo_cmaa_enable === "1" ? "开启" : "关闭",
|
||||||
|
options: ["关闭", "开启"],
|
||||||
|
mapping: (value: string) => {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
关闭: "0",
|
||||||
|
开启: "1",
|
||||||
|
}[value] || "0"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "msaa_samples",
|
||||||
|
title: "多重采样抗锯齿",
|
||||||
|
value:
|
||||||
|
{
|
||||||
|
0: "无",
|
||||||
|
2: "2X MSAA",
|
||||||
|
4: "4X MSAA",
|
||||||
|
8: "8X MSAA",
|
||||||
|
}[parseInt(video.msaa_samples, 10)] || "无",
|
||||||
|
options: ["无", "2X MSAA", "4X MSAA", "8X MSAA"],
|
||||||
|
mapping: (value: string) => {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
无: "0",
|
||||||
|
"2X MSAA": "2",
|
||||||
|
"4X MSAA": "4",
|
||||||
|
"8X MSAA": "8",
|
||||||
|
}[value] || "0"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "videocfg_shadow_quality",
|
||||||
|
title: "全局阴影效果",
|
||||||
|
value: ["低", "中", "高", "非常高"][parseInt(video.videocfg_shadow_quality, 10)] || "低",
|
||||||
|
options: ["低", "中", "高", "非常高"],
|
||||||
|
mapping: (value: string) => {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
低: "0",
|
||||||
|
中: "1",
|
||||||
|
高: "2",
|
||||||
|
非常高: "3",
|
||||||
|
}[value] || "0"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "videocfg_dynamic_shadows",
|
||||||
|
title: "动态阴影",
|
||||||
|
value: ["仅限日光", "全部"][parseInt(video.videocfg_dynamic_shadows, 10)] || "仅限日光",
|
||||||
|
options: ["仅限日光", "全部"],
|
||||||
|
mapping: (value: string) => {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
仅限日光: "0",
|
||||||
|
全部: "1",
|
||||||
|
}[value] || "0"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "videocfg_texture_detail",
|
||||||
|
title: "模型/贴图细节",
|
||||||
|
value: ["低", "中", "高"][parseInt(video.videocfg_texture_detail, 10)] || "低",
|
||||||
|
options: ["低", "中", "高"],
|
||||||
|
mapping: (value: string) => {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
低: "0",
|
||||||
|
中: "1",
|
||||||
|
高: "2",
|
||||||
|
}[value] || "0"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "r_texturefilteringquality",
|
||||||
|
title: "贴图过滤模式",
|
||||||
|
value:
|
||||||
|
["双线性", "三线性", "异向 2X", "异向 4X", "异向 8X", "异向 16X"][
|
||||||
|
parseInt(video.r_texturefilteringquality, 10)
|
||||||
|
] || "双线性",
|
||||||
|
options: ["双线性", "三线性", "异向 2X", "异向 4X", "异向 8X", "异向 16X"],
|
||||||
|
mapping: (value: string) => {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
双线性: "0",
|
||||||
|
三线性: "1",
|
||||||
|
"异向 2X": "2",
|
||||||
|
"异向 4X": "3",
|
||||||
|
"异向 8X": "4",
|
||||||
|
"异向 16X": "5",
|
||||||
|
}[value] || "0"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "shaderquality",
|
||||||
|
title: "光影细节",
|
||||||
|
value: video.shaderquality === "1" ? "高" : "低",
|
||||||
|
options: ["低", "高"],
|
||||||
|
mapping: (value: string) => {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
低: "0",
|
||||||
|
高: "1",
|
||||||
|
}[value] || "0"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "videocfg_particle_detail",
|
||||||
|
title: "粒子细节",
|
||||||
|
value: ["低", "中", "高", "非常高"][parseInt(video.videocfg_particle_detail, 10)] || "低",
|
||||||
|
options: ["低", "中", "高", "非常高"],
|
||||||
|
mapping: (value: string) => {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
低: "0",
|
||||||
|
中: "1",
|
||||||
|
高: "2",
|
||||||
|
非常高: "3",
|
||||||
|
}[value] || "低"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "videocfg_ao_detail",
|
||||||
|
title: "环境光遮蔽",
|
||||||
|
value: ["已禁用", "中", "高"][parseInt(video.videocfg_ao_detail, 10)] || "已禁用",
|
||||||
|
options: ["已禁用", "中", "高"],
|
||||||
|
mapping: (value: string) => {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
已禁用: "0",
|
||||||
|
中: "1",
|
||||||
|
高: "2",
|
||||||
|
}[value] || "已禁用"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "videocfg_hdr_detail",
|
||||||
|
title: "高动态范围",
|
||||||
|
value: video.videocfg_hdr_detail === "-1" ? "品质" : "性能",
|
||||||
|
options: ["性能", "品质"],
|
||||||
|
mapping: (value: string) => {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
性能: "3",
|
||||||
|
品质: "-1",
|
||||||
|
}[value] || "3"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "videocfg_fsr_detail",
|
||||||
|
title: "Fidelity FX 超级分辨率",
|
||||||
|
value:
|
||||||
|
["已禁用", "超高品质", "品质", "均衡", "性能"][parseInt(video.videocfg_fsr_detail, 10)] ||
|
||||||
|
"性能",
|
||||||
|
options: ["性能", "均衡", "品质", "超高品质", "已禁用"],
|
||||||
|
mapping: (value: string) => {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
已禁用: "0",
|
||||||
|
超高品质: "1",
|
||||||
|
品质: "2",
|
||||||
|
均衡: "3",
|
||||||
|
性能: "4",
|
||||||
|
}[value] || "已禁用"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const [vconfig, setVconfig] = useState<VideoConfig>(tool.state.videoSetting)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (steam.state.steamDirValid && steam.currentUser())
|
||||||
|
void tool.getVideoConfig(steam.state.steamDir, steam.currentUser()?.steam_id32 || 0)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const debounceCurrentUserId = useDebounce(steam.currentUser()?.steam_id32, {
|
||||||
|
wait: 500,
|
||||||
|
leading: false,
|
||||||
|
trailing: true,
|
||||||
|
maxWait: 2500,
|
||||||
|
})
|
||||||
|
// 节流重新读取配置函数,2秒间隔,trailing模式
|
||||||
|
const { run: throttledRefreshVideoConfig } = useThrottleFn(
|
||||||
|
async () => {
|
||||||
|
if (steam.state.steamDirValid && steam.currentUser()) {
|
||||||
|
await tool.getVideoConfig(
|
||||||
|
steam.state.steamDir,
|
||||||
|
steam.currentUser()?.steam_id32 || 0
|
||||||
|
)
|
||||||
|
// 如果不在编辑状态,更新本地状态(使用 ref 获取最新的 edit 值)
|
||||||
|
if (!editRef.current) {
|
||||||
|
setVconfig(tool.state.videoSetting)
|
||||||
|
}
|
||||||
|
addToast({ title: "检测到视频设置文件变动,已自动刷新", color: "success" })
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ type: "", title: "全局阴影效果", value: "低", options: ["低", "中", "高", "非常高"] },
|
|
||||||
{ type: "", title: "动态阴影", value: "全部", options: ["仅限日光", "全部"] },
|
|
||||||
{ type: "", title: "模型/贴图细节", value: "中", options: ["低", "中", "高"] },
|
|
||||||
{
|
{
|
||||||
type: "",
|
wait: 2000,
|
||||||
title: "贴图过滤模式",
|
leading: false,
|
||||||
value: "异向 4X",
|
trailing: true,
|
||||||
options: ["双线性", "三线性", "异向 2X", "异向 4X", "异向 8X", "异向 16X"],
|
}
|
||||||
},
|
)
|
||||||
{ type: "", title: "光影细节", value: "低", options: ["低", "高"] },
|
|
||||||
{ type: "", title: "粒子细节", value: "低", options: ["低", "中", "高", "非常高"] },
|
useEffect(() => {
|
||||||
{ type: "", title: "环境光遮蔽", value: "已禁用", options: ["已禁用", "中", "高"] },
|
if (steam.state.steamDirValid && steam.currentUser()) {
|
||||||
{ type: "", title: "高动态范围", value: "性能", options: ["性能", "品质"] },
|
// 安全地获取视频配置(内部已有错误处理)
|
||||||
{
|
void tool.getVideoConfig(steam.state.steamDir, steam.currentUser()?.steam_id32 || 0)
|
||||||
type: "",
|
// 启动文件监听,添加错误处理
|
||||||
title: "Fidelity FX 超级分辨率",
|
void invoke("start_watch_cs2_video", {
|
||||||
value: "已禁用",
|
steamDir: steam.state.steamDir,
|
||||||
options: ["性能", "均衡", "品质", "超高品质", "已禁用"],
|
steamId32: steam.currentUser()?.steam_id32 || 0,
|
||||||
},
|
}).catch((error) => {
|
||||||
]
|
console.error("启动视频配置文件监听失败:", error)
|
||||||
|
// 不显示错误提示,避免干扰用户
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 清理函数:停止后端监听
|
||||||
|
return () => {
|
||||||
|
void invoke("stop_watch_cs2_video").catch(() => {
|
||||||
|
// 忽略错误,可能监听器已经不存在
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [debounceCurrentUserId, steam.state.steamDirValid, steam.state.steamDir, tool])
|
||||||
|
|
||||||
|
// 监听 cs2_video.txt 文件变动事件
|
||||||
|
useEffect(() => {
|
||||||
|
const unlisten = listen("steam://cs2_video_changed", () => {
|
||||||
|
// 文件变化时使用节流函数重新读取配置
|
||||||
|
throttledRefreshVideoConfig()
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
void unlisten.then((fn) => fn())
|
||||||
|
}
|
||||||
|
}, [throttledRefreshVideoConfig])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="功能测试中,尚未实装" showArrow={true} delay={300}>
|
<Card>
|
||||||
<Card>
|
<CardHeader>
|
||||||
<CardHeader>
|
<CardIcon>
|
||||||
<CardIcon>
|
<SettingConfig /> 视频设置
|
||||||
<SettingConfig /> 视频设置
|
{isGameRunning && (
|
||||||
</CardIcon>
|
<Chip size="sm" color="warning" variant="flat" className="ml-2">
|
||||||
<CardTool>
|
游戏运行中
|
||||||
{/* {tool.state.VideoSettings.map((option, index) => (
|
</Chip>
|
||||||
|
)}
|
||||||
|
</CardIcon>
|
||||||
|
<CardTool>
|
||||||
|
{/* {tool.state.VideoSettings.map((option, index) => (
|
||||||
<ToolButton key={index} onClick={() => tool.setLaunchIndex(index)}>
|
<ToolButton key={index} onClick={() => tool.setLaunchIndex(index)}>
|
||||||
{index + 1}
|
{index + 1}
|
||||||
</ToolButton>
|
</ToolButton>
|
||||||
))} */}
|
))} */}
|
||||||
{edit && (
|
{edit && (
|
||||||
|
<>
|
||||||
|
<ToolButton onClick={() => setVconfig({ ...vconfig, ...VideoSettingTemplate.low })}>
|
||||||
|
低
|
||||||
|
</ToolButton>
|
||||||
|
<ToolButton
|
||||||
|
onClick={() => setVconfig({ ...vconfig, ...VideoSettingTemplate.middle })}
|
||||||
|
>
|
||||||
|
中
|
||||||
|
</ToolButton>
|
||||||
|
<ToolButton onClick={() => setVconfig({ ...vconfig, ...VideoSettingTemplate.high })}>
|
||||||
|
高
|
||||||
|
</ToolButton>
|
||||||
|
<ToolButton
|
||||||
|
onClick={() => setVconfig({ ...vconfig, ...VideoSettingTemplate.veryhigh })}
|
||||||
|
>
|
||||||
|
非常高
|
||||||
|
</ToolButton>
|
||||||
|
<ToolButton
|
||||||
|
onClick={() => setVconfig({ ...vconfig, ...VideoSettingTemplate.recommend })}
|
||||||
|
>
|
||||||
|
推荐
|
||||||
|
</ToolButton>
|
||||||
|
<Tooltip
|
||||||
|
content={isGameRunning ? "游戏运行中,无法修改视频设置" : ""}
|
||||||
|
isDisabled={!isGameRunning}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<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,
|
||||||
|
vconfig
|
||||||
|
)
|
||||||
|
await tool.getVideoConfig(
|
||||||
|
steam.state.steamDir,
|
||||||
|
steam.currentUser()?.steam_id32 || 0
|
||||||
|
)
|
||||||
|
setEdit(false)
|
||||||
|
addToast({ title: "应用设置成功" })
|
||||||
|
}}
|
||||||
|
disabled={isGameRunning}
|
||||||
|
>
|
||||||
|
<Plus />
|
||||||
|
应用
|
||||||
|
</ToolButton>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<ToolButton
|
||||||
|
onClick={async () => {
|
||||||
|
if (steam.state.steamDirValid && steam.currentUser())
|
||||||
|
await tool.getVideoConfig(
|
||||||
|
steam.state.steamDir,
|
||||||
|
steam.currentUser()?.steam_id32 || 0
|
||||||
|
)
|
||||||
|
setVconfig(tool.state.videoSetting)
|
||||||
|
setEdit(!edit)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{edit ? (
|
||||||
<>
|
<>
|
||||||
<ToolButton>低</ToolButton>
|
<CloseSmall />
|
||||||
<ToolButton>中</ToolButton>
|
取消编辑
|
||||||
<ToolButton>高</ToolButton>
|
</>
|
||||||
<ToolButton>非常高</ToolButton>
|
) : (
|
||||||
<ToolButton>推荐</ToolButton>
|
<>
|
||||||
<ToolButton
|
<Edit />
|
||||||
onClick={() => {
|
编辑
|
||||||
addToast({ title: "测试中 功能完成后可应用设置到游戏" })
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Plus />
|
|
||||||
应用
|
|
||||||
</ToolButton>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<ToolButton onClick={() => setEdit(!edit)}>
|
</ToolButton>
|
||||||
{edit ? (
|
|
||||||
<>
|
<ToolButton onClick={debouncedGetVideoConfig}>读取</ToolButton>
|
||||||
<CloseSmall />
|
<ToolButton onClick={() => setHide(!hide)}>
|
||||||
取消编辑
|
{hide ? (
|
||||||
</>
|
<>
|
||||||
) : (
|
<Up />
|
||||||
<>
|
显示
|
||||||
<Edit />
|
</>
|
||||||
编辑
|
) : (
|
||||||
</>
|
<>
|
||||||
)}
|
<Down />
|
||||||
</ToolButton>
|
隐藏
|
||||||
<ToolButton onClick={() => setHide(!hide)}>
|
</>
|
||||||
{hide ? (
|
)}
|
||||||
<>
|
</ToolButton>
|
||||||
<Up />
|
</CardTool>
|
||||||
显示
|
</CardHeader>
|
||||||
</>
|
{!hide && (
|
||||||
) : (
|
<motion.div
|
||||||
<>
|
initial={{ opacity: 0 }}
|
||||||
<Down />
|
animate={{ opacity: 1 }}
|
||||||
隐藏
|
exit={{ opacity: 0 }}
|
||||||
</>
|
transition={{ duration: 0.2 }}
|
||||||
)}
|
>
|
||||||
</ToolButton>
|
<CardBody>
|
||||||
</CardTool>
|
{edit ? (
|
||||||
</CardHeader>
|
// 编辑状态:显示完整的可编辑控件
|
||||||
{!hide && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
|
||||||
<CardBody>
|
|
||||||
<ul className="flex flex-wrap gap-3 mt-1">
|
<ul className="flex flex-wrap gap-3 mt-1">
|
||||||
<li className="flex flex-col gap-1.5">
|
<li className="flex flex-col gap-1.5">
|
||||||
<span className="ml-2">分辨率</span>
|
<div className="flex items-center gap-2 ml-2">
|
||||||
<span className="flex gap-3">
|
<span>分辨率</span>
|
||||||
<NumberInput
|
<Dropdown placement="bottom-start" className="min-w-fit">
|
||||||
aria-label="width"
|
<DropdownTrigger>
|
||||||
value={tool.state.videoSetting.width}
|
<Button
|
||||||
onValueChange={(value) => {
|
size="sm"
|
||||||
tool.setVideoSetting({
|
variant="flat"
|
||||||
...tool.state.videoSetting,
|
className="h-5 min-w-[50px] px-1.5 text-xs"
|
||||||
width: value,
|
>
|
||||||
|
预设
|
||||||
|
</Button>
|
||||||
|
</DropdownTrigger>
|
||||||
|
<DropdownMenu aria-label="预设分辨率" className="">
|
||||||
|
{PRESET_RESOLUTIONS.map((preset) => (
|
||||||
|
<DropdownItem
|
||||||
|
key={preset.label}
|
||||||
|
onPress={() => {
|
||||||
|
setVconfig({
|
||||||
|
...vconfig,
|
||||||
|
defaultres: preset.width,
|
||||||
|
defaultresheight: preset.height,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{preset.label}
|
||||||
|
</DropdownItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Input
|
||||||
|
size="sm"
|
||||||
|
type="number"
|
||||||
|
placeholder="宽"
|
||||||
|
value={vconfig.defaultres}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
setVconfig({
|
||||||
|
...vconfig,
|
||||||
|
defaultres: val,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
radius="full"
|
radius="full"
|
||||||
step={10}
|
className="w-20"
|
||||||
className="max-w-28"
|
classNames={{
|
||||||
classNames={{ inputWrapper: "h-10" }}
|
inputWrapper: "h-9 px-3",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<span className="text-xs text-default-400">x</span>
|
||||||
aria-label="height"
|
<Input
|
||||||
value={tool.state.videoSetting.height}
|
size="sm"
|
||||||
onValueChange={(value) => {
|
type="number"
|
||||||
tool.setVideoSetting({
|
placeholder="高"
|
||||||
...tool.state.videoSetting,
|
value={vconfig.defaultresheight}
|
||||||
height: value,
|
onValueChange={(val) => {
|
||||||
|
setVconfig({
|
||||||
|
...vconfig,
|
||||||
|
defaultresheight: val,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
radius="full"
|
radius="full"
|
||||||
step={10}
|
className="w-20"
|
||||||
className="max-w-28"
|
classNames={{
|
||||||
classNames={{ inputWrapper: "h-10" }}
|
inputWrapper: "h-9 px-3",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
{videoSettings.map((vid, index) => (
|
{videoSettings(vconfig).map((vid, index) => (
|
||||||
<li className="flex flex-col gap-1.5" key={index}>
|
<li className="flex flex-col gap-1.5" key={index}>
|
||||||
<span className="ml-2">{vid.title}</span>
|
<span className="ml-2">{vid.title}</span>
|
||||||
<Tabs
|
<Tabs
|
||||||
selectedKey={vid.value}
|
size="sm"
|
||||||
size="md"
|
|
||||||
radius="full"
|
radius="full"
|
||||||
className="min-w-36"
|
className="min-w-36"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
selectedKey={vid.value}
|
||||||
|
onSelectionChange={(key) => {
|
||||||
|
if (key) {
|
||||||
|
const mappedValue = vid.mapping(key.toString())
|
||||||
|
// 如果返回的是对象(如全屏模式),需要同时更新多个字段
|
||||||
|
if (typeof mappedValue === "object" && mappedValue !== null) {
|
||||||
|
setVconfig({
|
||||||
|
...vconfig,
|
||||||
|
...mappedValue,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 否则只更新单个字段
|
||||||
|
setVconfig({
|
||||||
|
...vconfig,
|
||||||
|
[vid.type]: mappedValue,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{vid.options.map((opt, _) => (
|
{vid.options.map((opt, _) => (
|
||||||
<Tab key={opt} title={opt} />
|
<Tab key={opt} title={opt} titleValue={opt} />
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</CardBody>
|
) : (
|
||||||
</motion.div>
|
// 非编辑状态:显示精简的只读信息
|
||||||
)}
|
<div className="mt-1">
|
||||||
</Card>
|
<div className="grid grid-cols-3 md:grid-cols-4 gap-2.5">
|
||||||
</Tooltip>
|
<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>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { Code, Link } from '@heroui/react'
|
import { Code, Link } from "@heroui/react"
|
||||||
import Markdown from 'react-markdown'
|
import Markdown from "react-markdown"
|
||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from "remark-gfm"
|
||||||
|
|
||||||
export const components = {
|
export const components = {
|
||||||
a: ({ href, children }: { href: string; children: React.ReactNode }) => (
|
a: ({ href, children }: { href: string; children: React.ReactNode }) => (
|
||||||
@@ -10,13 +10,23 @@ export const components = {
|
|||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
// img: ({ src, alt }: { src: string; alt: string }) => <Image src={src} alt={alt} className="object-cover w-full h-full" />,
|
// img: ({ src, alt }: { src: string; alt: string }) => <Image src={src} alt={alt} className="object-cover w-full h-full" />,
|
||||||
h1: ({ children }: { children: React.ReactNode }) => <h1 className="text-2xl font-bold mb-2.5">{children}</h1>,
|
h1: ({ children }: { children: React.ReactNode }) => (
|
||||||
h2: ({ children }: { children: React.ReactNode }) => <h2 className="text-xl font-semibold mb-2.5">{children}</h2>,
|
<h1 className="text-2xl font-bold mb-2.5">{children}</h1>
|
||||||
h3: ({ children }: { children: React.ReactNode }) => <h3 className="text-lg font-medium mb-2.5">{children}</h3>,
|
),
|
||||||
p: ({ children }: { children: React.ReactNode }) => <p className="mb-2.5 text-base">{children}</p>,
|
h2: ({ children }: { children: React.ReactNode }) => (
|
||||||
ul: ({ children }: { children: React.ReactNode }) => <ul className="list-disc pl-6 mb-2.5">{children}</ul>,
|
<h2 className="text-xl font-semibold mb-2.5">{children}</h2>
|
||||||
li: ({ children }: { children: React.ReactNode }) => <li className="mb-2">{children}</li>,
|
),
|
||||||
code: ({ children }: { children: React.ReactNode }) => <Code size="sm" >{children}</Code>,
|
h3: ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<h3 className="text-lg font-medium mb-2.5">{children}</h3>
|
||||||
|
),
|
||||||
|
p: ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<p className="mb-2.5 text-base">{children}</p>
|
||||||
|
),
|
||||||
|
ul: ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<ul className="pl-6 mt-2 mb-3 list-disc">{children}</ul>
|
||||||
|
),
|
||||||
|
li: ({ children }: { children: React.ReactNode }) => <li className="mb-1.5 mt-1">{children}</li>,
|
||||||
|
code: ({ children }: { children: React.ReactNode }) => <Code size="sm">{children}</Code>,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MarkdownRender({ children }: { children: React.ReactNode }) {
|
export function MarkdownRender({ children }: { children: React.ReactNode }) {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const Card = ({ children, className, ...props }: CardProps) => {
|
|||||||
|
|
||||||
const CardHeader = ({ children }: CardProps) => {
|
const CardHeader = ({ children }: CardProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1.5 tracking-wide select-none">
|
<div className="flex items-center gap-1.5 tracking-wide select-none shrink-0">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -34,7 +34,7 @@ const CardIcon = ({ children, type, className, ...rest }: CardProps) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex gap-1.5 items-center font-semibold",
|
"flex gap-1.5 items-center font-semibold flex-shrink-0",
|
||||||
type === "menu" &&
|
type === "menu" &&
|
||||||
"transition cursor-pointer hover:bg-black/5 dark:hover:bg-white/5 px-2 py-1 rounded-md active:scale-95",
|
"transition cursor-pointer hover:bg-black/5 dark:hover:bg-white/5 px-2 py-1 rounded-md active:scale-95",
|
||||||
className,
|
className,
|
||||||
@@ -46,9 +46,9 @@ const CardIcon = ({ children, type, className, ...rest }: CardProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CardTool = ({ children }: CardProps) => {
|
const CardTool = ({ children, className }: CardProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-end flex-grow gap-2">
|
<div className={cn("flex items-center justify-end flex-grow gap-2", className)}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { setTheme as setTauriTheme } from "@/hooks/tauri/theme"
|
import { setTheme as setTauriTheme } from "@/hooks/tauri/theme" //'@tauri-apps/api/app'
|
||||||
import { useAppStore } from "@/store/app"
|
import { useAppStore } from "@/store/app"
|
||||||
import { useToolStore } from "@/store/tool"
|
import { useToolStore } from "@/store/tool"
|
||||||
import { addToast, Button, Link, Tooltip, useDisclosure } from "@heroui/react"
|
import { addToast, Button, Link, Tooltip, useDisclosure } from "@heroui/react"
|
||||||
@@ -11,8 +11,7 @@ import {
|
|||||||
Refresh,
|
Refresh,
|
||||||
RocketOne,
|
RocketOne,
|
||||||
Square,
|
Square,
|
||||||
SunOne,
|
SunOne
|
||||||
SurprisedFaceWithOpenBigMouth,
|
|
||||||
} from "@icon-park/react"
|
} from "@icon-park/react"
|
||||||
import { type Theme, getCurrentWindow } from "@tauri-apps/api/window"
|
import { type Theme, getCurrentWindow } from "@tauri-apps/api/window"
|
||||||
import { /* relaunch, */ exit } from "@tauri-apps/plugin-process"
|
import { /* relaunch, */ exit } from "@tauri-apps/plugin-process"
|
||||||
@@ -21,6 +20,8 @@ import { usePathname, useRouter } from "next/navigation"
|
|||||||
import { saveAllNow } from "@tauri-store/valtio"
|
import { saveAllNow } from "@tauri-store/valtio"
|
||||||
import { useSteamStore } from "@/store/steam"
|
import { useSteamStore } from "@/store/steam"
|
||||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/react"
|
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/react"
|
||||||
|
import { window } from "@tauri-apps/api"
|
||||||
|
import { AuthButton } from "@/components/auth/AuthButton"
|
||||||
|
|
||||||
const Nav = () => {
|
const Nav = () => {
|
||||||
const { theme, setTheme } = useTheme()
|
const { theme, setTheme } = useTheme()
|
||||||
@@ -29,10 +30,12 @@ const Nav = () => {
|
|||||||
await setTauriTheme(theme)
|
await setTauriTheme(theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const app = useAppStore()
|
||||||
const close = async () => {
|
const close = async () => {
|
||||||
// (await window.hideOnClose) ? getCurrent().hide() : exit();
|
|
||||||
await saveAllNow()
|
await saveAllNow()
|
||||||
await exit()
|
// await exit()
|
||||||
|
if (app.state.hiddenOnClose) await window.getCurrentWindow().hide()
|
||||||
|
else await exit()
|
||||||
}
|
}
|
||||||
|
|
||||||
const minimize = async () => {
|
const minimize = async () => {
|
||||||
@@ -52,69 +55,69 @@ const Nav = () => {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
|
|
||||||
const app = useAppStore()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="absolute top-0 right-0 flex flex-row h-16 gap-0.5 p-4" data-tauri-drag-region>
|
<nav className="absolute top-0 right-0 flex flex-row h-16 gap-0.5 p-4" data-tauri-drag-region>
|
||||||
<Tooltip content="启动页确认设置" showArrow={true} delay={300}>
|
<Tooltip content="启动页确认设置" showArrow={true} delay={300}>
|
||||||
{pathname !== "/" && (
|
{pathname !== "/" && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="px-2 py-0 transition duration-150 rounded hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
|
className="px-2 py-0 transition duration-150 rounded cursor-pointer hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
app.setInited(false)
|
app.setInited(false)
|
||||||
if (pathname !== "/") router.push("/")
|
if (pathname !== "/") router.push("/")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RocketOne size={16} />
|
<RocketOne size={16} className="cursor-pointer" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip content="深色模式" showArrow={true} delay={300}>
|
<Tooltip content="深色模式" showArrow={true} delay={300}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="px-2 py-0 transition duration-150 rounded hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
|
className="px-2 py-0 transition duration-150 rounded cursor-pointer hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
|
||||||
onClick={() => (theme === "light" ? setAppTheme("dark") : setAppTheme("light"))}
|
onClick={() => (theme === "light" ? setAppTheme("dark") : setAppTheme("light"))}
|
||||||
>
|
>
|
||||||
{theme === "light" ? <SunOne size={16} /> : <Moon size={16} />}
|
{theme === "light" ? <SunOne size={16} className="cursor-pointer" /> : <Moon size={16} className="cursor-pointer" />}
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip content="反馈" showArrow={true} delay={300}>
|
<Tooltip content="反馈" showArrow={true} delay={300}>
|
||||||
<Link
|
<Link
|
||||||
href="https://docs.qq.com/form/page/DZU1ieW9SQkxWU1RF"
|
href="https://docs.qq.com/form/page/DZU1ieW9SQkxWU1RF"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="px-2 py-0 text-black transition duration-150 rounded dark:text-white hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
|
className="px-2 py-0 text-black transition duration-150 rounded cursor-pointer dark:text-white hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
|
||||||
>
|
>
|
||||||
<button type="button">
|
<button type="button" className="cursor-pointer">
|
||||||
<Communication size={16} />
|
<Communication size={16} className="cursor-pointer" />
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* <AuthButtonWrapper /> */}
|
||||||
|
|
||||||
<ResetModal />
|
<ResetModal />
|
||||||
|
|
||||||
{/* { platform() === "windows" && ( */}
|
{/* { platform() === "windows" && ( */}
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="px-2 py-0 transition duration-150 rounded hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
|
className="px-2 py-0 transition duration-150 rounded cursor-pointer hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
|
||||||
onClick={minimize}
|
onClick={minimize}
|
||||||
>
|
>
|
||||||
<Minus size={16} />
|
<Minus size={16} className="cursor-pointer" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="px-2 py-0 transition duration-150 rounded hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
|
className="px-2 py-0 transition duration-150 rounded cursor-pointer hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
|
||||||
onClick={toggleMaximize}
|
onClick={toggleMaximize}
|
||||||
>
|
>
|
||||||
<Square size={16} />
|
<Square size={16} className="cursor-pointer" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="px-2 py-0 transition duration-150 rounded hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
|
className="px-2 py-0 transition duration-150 rounded cursor-pointer hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
|
||||||
onClick={close}
|
onClick={close}
|
||||||
>
|
>
|
||||||
<Close size={16} />
|
<Close size={16} className="cursor-pointer" />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
{/* )} */}
|
{/* )} */}
|
||||||
@@ -146,10 +149,10 @@ function ResetModal() {
|
|||||||
<Tooltip content="重置设置" showArrow={true} delay={300}>
|
<Tooltip content="重置设置" showArrow={true} delay={300}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="px-2 py-0 transition duration-150 rounded hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
|
className="px-2 py-0 transition duration-150 rounded cursor-pointer hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
|
||||||
onClick={onOpen}
|
onClick={onOpen}
|
||||||
>
|
>
|
||||||
<Refresh size={16} />
|
<Refresh size={16} className="cursor-pointer" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
@@ -182,4 +185,8 @@ function ResetModal() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AuthButtonWrapper() {
|
||||||
|
return <AuthButton />
|
||||||
|
}
|
||||||
|
|
||||||
export default Nav
|
export default Nav
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { cn, user } from "@heroui/react"
|
import { cn, Tooltip } from "@heroui/react"
|
||||||
import { Home, MonitorOne, Movie, Setting, Terminal, Toolkit } from "@icon-park/react"
|
import { Home, MonitorOne, Movie, NewspaperFolding, Setting, Terminal, Toolkit } from "@icon-park/react"
|
||||||
import { usePathname, useRouter } from "next/navigation"
|
import { usePathname, useRouter } from "next/navigation"
|
||||||
import type { ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
import { getVersion } from "@tauri-apps/api/app"
|
import { getVersion } from "@tauri-apps/api/app"
|
||||||
@@ -9,6 +9,17 @@ import { getVersion } from "@tauri-apps/api/app"
|
|||||||
import { useAppStore } from "@/store/app"
|
import { useAppStore } from "@/store/app"
|
||||||
import { useSteamStore } from "@/store/steam"
|
import { useSteamStore } from "@/store/steam"
|
||||||
|
|
||||||
|
// 路由到页面名称的映射
|
||||||
|
const routeNames: Record<string, string> = {
|
||||||
|
"/home": "首页",
|
||||||
|
"/dynamic": "动态",
|
||||||
|
"/tool": "工具",
|
||||||
|
"/console": "控制台",
|
||||||
|
"/gear": "硬件外设",
|
||||||
|
"/movie": "录像",
|
||||||
|
"/preference": "偏好设置",
|
||||||
|
}
|
||||||
|
|
||||||
interface SideButtonProps {
|
interface SideButtonProps {
|
||||||
route: string
|
route: string
|
||||||
className?: string
|
className?: string
|
||||||
@@ -23,26 +34,29 @@ const SideButton = ({
|
|||||||
}: SideButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
}: SideButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const path = usePathname()
|
const path = usePathname()
|
||||||
|
const pageName = routeNames[route] || route
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<Tooltip content={pageName} showArrow={true} delay={300} placement="right">
|
||||||
type="button"
|
<button
|
||||||
onClick={() => router.push(route || "/")}
|
type="button"
|
||||||
className={cn(
|
onClick={() => router.push(route || "/")}
|
||||||
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
|
|
||||||
className={cn(
|
className={cn(
|
||||||
path.startsWith(route) && "opacity-100",
|
className,
|
||||||
"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"
|
"p-2.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition relative active:scale-90 cursor-pointer [&>*]:cursor-pointer",
|
||||||
|
path.startsWith(route) && "bg-black/5 dark:bg-white/5"
|
||||||
)}
|
)}
|
||||||
/>
|
{...rest}
|
||||||
</button>
|
>
|
||||||
|
{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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +69,7 @@ const Avatar = () => {
|
|||||||
<img
|
<img
|
||||||
src={
|
src={
|
||||||
steam.currentUser()?.avatar
|
steam.currentUser()?.avatar
|
||||||
? `data:image/png;base64,${steam.currentUser()?.avatar || ''}`
|
? `data:image/png;base64,${steam.currentUser()?.avatar || ""}`
|
||||||
: "/logo_square.png"
|
: "/logo_square.png"
|
||||||
}
|
}
|
||||||
alt="avatar"
|
alt="avatar"
|
||||||
@@ -72,9 +86,10 @@ const Avatar = () => {
|
|||||||
const SideBar = () => {
|
const SideBar = () => {
|
||||||
const app = useAppStore()
|
const app = useAppStore()
|
||||||
|
|
||||||
void getVersion().then((Value) => {
|
if (typeof window !== "undefined")
|
||||||
app.setVersion(Value)
|
void getVersion().then((Value) => {
|
||||||
})
|
app.setVersion(Value)
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -89,12 +104,15 @@ const SideBar = () => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
className="flex flex-col items-center justify-center h-full gap-5"
|
className="flex flex-col items-center justify-center h-full gap-5 pt-5"
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
>
|
>
|
||||||
<SideButton route="/home">
|
<SideButton route="/home">
|
||||||
<Home size={24} />
|
<Home size={24} />
|
||||||
</SideButton>
|
</SideButton>
|
||||||
|
<SideButton route="/dynamic">
|
||||||
|
<NewspaperFolding size={24} />
|
||||||
|
</SideButton>
|
||||||
<SideButton route="/tool">
|
<SideButton route="/tool">
|
||||||
<Toolkit size={24} />
|
<Toolkit size={24} />
|
||||||
</SideButton>
|
</SideButton>
|
||||||
@@ -114,7 +132,7 @@ const SideBar = () => {
|
|||||||
|
|
||||||
<div className="mx-auto text-sm text-center text-zinc-500" data-tauri-drag-region>
|
<div className="mx-auto text-sm text-center text-zinc-500" data-tauri-drag-region>
|
||||||
<p>版本号</p>
|
<p>版本号</p>
|
||||||
<p className="py-1 text-sm text-zinc-600">{app.state.version}</p>
|
<p className="py-1 text-xs text-zinc-600">{app.state.version}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
|
import { cn } from "@heroui/react"
|
||||||
import type { ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
|
|
||||||
interface ToolButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
interface ToolButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
|
className?: string
|
||||||
|
selected?: boolean
|
||||||
}
|
}
|
||||||
export const ToolButton = ({ children, ...rest }: ToolButtonProps) => {
|
export const ToolButton = ({ children, className, selected, disabled, ...rest }: ToolButtonProps) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex gap-0.5 active:scale-95 items-center min-w-7 justify-center px-2 py-1.5 bg-black/5 transition hover:bg-black/10 dark:bg-white/5 dark:hover:bg-white/10 rounded-md text-sm leading-none"
|
disabled={disabled}
|
||||||
|
className={cn(
|
||||||
|
"flex shrink-0 gap-0.5 items-center min-w-7 justify-center px-2 py-1.5 bg-black/5 transition rounded-md text-sm leading-none",
|
||||||
|
disabled
|
||||||
|
? "opacity-50 cursor-not-allowed"
|
||||||
|
: "active:scale-95 cursor-pointer hover:bg-black/10 dark:hover:bg-white/10",
|
||||||
|
"dark:bg-white/5",
|
||||||
|
className,
|
||||||
|
selected &&
|
||||||
|
"bg-purple-500/40 hover:bg-purple-500/20 text-purple-900 dark:text-purple-100 drop-shadow-sm dark:bg-purple-500/40 dark:hover:bg-purple-500/20"
|
||||||
|
)}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
65
src/hooks/useGlobalGameMonitor.ts
Normal file
65
src/hooks/useGlobalGameMonitor.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { useEffect, useRef } from "react"
|
||||||
|
import { useToolStore } from "@/store/tool"
|
||||||
|
|
||||||
|
// 全局检测间隔(毫秒)- 增加到5秒以减少性能影响
|
||||||
|
const CHECK_INTERVAL = 5000
|
||||||
|
|
||||||
|
// 全局检测状态管理
|
||||||
|
let globalInterval: NodeJS.Timeout | null = null
|
||||||
|
let subscriberCount = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局游戏状态监控 Hook
|
||||||
|
* 多个组件可以共享同一个检测循环,避免重复检测
|
||||||
|
*
|
||||||
|
* @param enabled 是否启用检测(默认 true)
|
||||||
|
* @returns 游戏运行状态和手动检测函数
|
||||||
|
*/
|
||||||
|
export function useGlobalGameMonitor(enabled: boolean = true) {
|
||||||
|
const tool = useToolStore()
|
||||||
|
const isGameRunning = tool.state.isGameRunning
|
||||||
|
const checkGameRunning = tool.checkGameRunning
|
||||||
|
const enabledRef = useRef(enabled)
|
||||||
|
|
||||||
|
// 更新 enabled 引用
|
||||||
|
useEffect(() => {
|
||||||
|
enabledRef.current = enabled
|
||||||
|
}, [enabled])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 增加订阅者计数
|
||||||
|
subscriberCount++
|
||||||
|
|
||||||
|
// 如果这是第一个订阅者,启动全局检测循环
|
||||||
|
if (subscriberCount === 1) {
|
||||||
|
// 立即检测一次
|
||||||
|
void checkGameRunning()
|
||||||
|
|
||||||
|
// 启动定期检测
|
||||||
|
globalInterval = setInterval(() => {
|
||||||
|
void checkGameRunning()
|
||||||
|
}, CHECK_INTERVAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理函数:减少订阅者计数
|
||||||
|
return () => {
|
||||||
|
subscriberCount--
|
||||||
|
|
||||||
|
// 如果没有订阅者了,停止检测循环
|
||||||
|
if (subscriberCount === 0 && globalInterval) {
|
||||||
|
clearInterval(globalInterval)
|
||||||
|
globalInterval = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [enabled, checkGameRunning])
|
||||||
|
|
||||||
|
return {
|
||||||
|
isGameRunning,
|
||||||
|
checkGameRunning,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,17 +1,36 @@
|
|||||||
import { store } from "@tauri-store/valtio"
|
import { store } from "@tauri-store/valtio"
|
||||||
import { useSnapshot } from "valtio"
|
import { useSnapshot } from "valtio"
|
||||||
import { DEFAULT_STORE_CONFIG } from "./config"
|
import { DEFAULT_STORE_CONFIG } from "./config"
|
||||||
import { enable, isEnabled, disable } from "@tauri-apps/plugin-autostart"
|
import { enable, disable } from "@tauri-apps/plugin-autostart"
|
||||||
|
import { LazyStore } from '@tauri-apps/plugin-store';
|
||||||
|
|
||||||
|
interface UpdateInfo {
|
||||||
|
version: string
|
||||||
|
notes?: string
|
||||||
|
download_url: string
|
||||||
|
}
|
||||||
|
|
||||||
const defaultValue = {
|
const defaultValue = {
|
||||||
version: "0.0.1",
|
version: "0.0.1",
|
||||||
hasUpdate: false,
|
hasUpdate: false,
|
||||||
|
latestVersion: "", // 最新版本号
|
||||||
|
updateInfo: null as UpdateInfo | null, // 更新信息
|
||||||
|
downloading: false, // 是否正在下载
|
||||||
|
downloadProgress: 0, // 下载进度 0-100
|
||||||
|
downloadCompleted: false, // 下载是否完成
|
||||||
inited: false,
|
inited: false,
|
||||||
notice: "",
|
notice: "",
|
||||||
useMirror: true,
|
useMirror: true, // 默认使用镜像源(CDN 加速)
|
||||||
|
includePrerelease: false, // 默认不包含预发布版本
|
||||||
|
useCdn: true, // 默认使用 CDN 加速下载
|
||||||
autoStart: false,
|
autoStart: false,
|
||||||
|
startHidden: false,
|
||||||
|
hiddenOnClose: false,
|
||||||
|
steamUsersViewMode: "list-large" as "card" | "list" | "list-large",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type { UpdateInfo }
|
||||||
|
|
||||||
export const appStore = store("app", { ...defaultValue }, DEFAULT_STORE_CONFIG)
|
export const appStore = store("app", { ...defaultValue }, DEFAULT_STORE_CONFIG)
|
||||||
|
|
||||||
export const useAppStore = () => {
|
export const useAppStore = () => {
|
||||||
@@ -23,20 +42,48 @@ export const useAppStore = () => {
|
|||||||
store: appStore,
|
store: appStore,
|
||||||
setVersion,
|
setVersion,
|
||||||
setHasUpdate,
|
setHasUpdate,
|
||||||
|
setLatestVersion,
|
||||||
|
setUpdateInfo,
|
||||||
|
setDownloading,
|
||||||
|
setDownloadProgress,
|
||||||
|
setDownloadCompleted,
|
||||||
setInited,
|
setInited,
|
||||||
setNotice,
|
setNotice,
|
||||||
setUseMirror,
|
setUseMirror,
|
||||||
|
setIncludePrerelease,
|
||||||
|
setUseCdn,
|
||||||
setAutoStart,
|
setAutoStart,
|
||||||
|
setStartHidden,
|
||||||
|
setHiddenOnClose,
|
||||||
|
setSteamUsersViewMode,
|
||||||
resetAppStore,
|
resetAppStore,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const launchStore = new LazyStore('cstb.json', { autoSave: true, defaults: defaultValue });
|
||||||
|
if (typeof window !== 'undefined') void launchStore.save()
|
||||||
|
|
||||||
const setVersion = (version: string) => {
|
const setVersion = (version: string) => {
|
||||||
appStore.state.version = version
|
appStore.state.version = version
|
||||||
}
|
}
|
||||||
const setHasUpdate = (hasUpdate: boolean) => {
|
const setHasUpdate = (hasUpdate: boolean) => {
|
||||||
appStore.state.hasUpdate = hasUpdate
|
appStore.state.hasUpdate = hasUpdate
|
||||||
}
|
}
|
||||||
|
const setLatestVersion = (latestVersion: string) => {
|
||||||
|
appStore.state.latestVersion = latestVersion
|
||||||
|
}
|
||||||
|
const setUpdateInfo = (updateInfo: UpdateInfo | null) => {
|
||||||
|
appStore.state.updateInfo = updateInfo
|
||||||
|
}
|
||||||
|
const setDownloading = (downloading: boolean) => {
|
||||||
|
appStore.state.downloading = downloading
|
||||||
|
}
|
||||||
|
const setDownloadProgress = (downloadProgress: number) => {
|
||||||
|
appStore.state.downloadProgress = downloadProgress
|
||||||
|
}
|
||||||
|
const setDownloadCompleted = (downloadCompleted: boolean) => {
|
||||||
|
appStore.state.downloadCompleted = downloadCompleted
|
||||||
|
}
|
||||||
const setInited = (inited: boolean) => {
|
const setInited = (inited: boolean) => {
|
||||||
appStore.state.inited = inited
|
appStore.state.inited = inited
|
||||||
}
|
}
|
||||||
@@ -46,6 +93,12 @@ const setNotice = (notice: string) => {
|
|||||||
const setUseMirror = (useMirror: boolean) => {
|
const setUseMirror = (useMirror: boolean) => {
|
||||||
appStore.state.useMirror = useMirror
|
appStore.state.useMirror = useMirror
|
||||||
}
|
}
|
||||||
|
const setIncludePrerelease = (includePrerelease: boolean) => {
|
||||||
|
appStore.state.includePrerelease = includePrerelease
|
||||||
|
}
|
||||||
|
const setUseCdn = (useCdn: boolean) => {
|
||||||
|
appStore.state.useCdn = useCdn
|
||||||
|
}
|
||||||
|
|
||||||
const setAutoStart = (autoStart: boolean) => {
|
const setAutoStart = (autoStart: boolean) => {
|
||||||
if (autoStart) {
|
if (autoStart) {
|
||||||
@@ -56,11 +109,36 @@ const setAutoStart = (autoStart: boolean) => {
|
|||||||
appStore.state.autoStart = autoStart
|
appStore.state.autoStart = autoStart
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 同步到 launchStore 使 start hidden 生效
|
||||||
|
const setStartHidden = async (startHidden: boolean) => {
|
||||||
|
appStore.state.startHidden = startHidden;
|
||||||
|
await launchStore.set('hidden', startHidden);
|
||||||
|
await launchStore.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
const setHiddenOnClose = (hiddenOnClose: boolean) => {
|
||||||
|
appStore.state.hiddenOnClose = hiddenOnClose;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setSteamUsersViewMode = (viewMode: "card" | "list" | "list-large") => {
|
||||||
|
appStore.state.steamUsersViewMode = viewMode
|
||||||
|
}
|
||||||
|
|
||||||
const resetAppStore = () => {
|
const resetAppStore = () => {
|
||||||
setVersion(defaultValue.version)
|
setVersion(defaultValue.version)
|
||||||
setHasUpdate(defaultValue.hasUpdate)
|
setHasUpdate(defaultValue.hasUpdate)
|
||||||
|
setLatestVersion(defaultValue.latestVersion)
|
||||||
|
setUpdateInfo(defaultValue.updateInfo)
|
||||||
|
setDownloading(defaultValue.downloading)
|
||||||
|
setDownloadProgress(defaultValue.downloadProgress)
|
||||||
|
setDownloadCompleted(defaultValue.downloadCompleted)
|
||||||
setInited(defaultValue.inited)
|
setInited(defaultValue.inited)
|
||||||
setNotice(defaultValue.notice)
|
setNotice(defaultValue.notice)
|
||||||
setUseMirror(defaultValue.useMirror)
|
setUseMirror(defaultValue.useMirror)
|
||||||
|
setIncludePrerelease(defaultValue.includePrerelease)
|
||||||
|
setUseCdn(defaultValue.useCdn)
|
||||||
setAutoStart(defaultValue.autoStart)
|
setAutoStart(defaultValue.autoStart)
|
||||||
|
void setStartHidden(defaultValue.startHidden)
|
||||||
|
setHiddenOnClose(defaultValue.hiddenOnClose)
|
||||||
|
setSteamUsersViewMode(defaultValue.steamUsersViewMode)
|
||||||
}
|
}
|
||||||
76
src/store/auth.ts
Normal file
76
src/store/auth.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { store } from "@tauri-store/valtio"
|
||||||
|
import { useSnapshot } from "valtio"
|
||||||
|
import { DEFAULT_STORE_CONFIG } from "./config"
|
||||||
|
import { createClient } from "@/utils/supabase/client"
|
||||||
|
import type { User, Session } from "@supabase/supabase-js"
|
||||||
|
|
||||||
|
interface AuthState {
|
||||||
|
user: User | null
|
||||||
|
session: Session | null
|
||||||
|
isLoading: boolean
|
||||||
|
isAuthenticated: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValue: AuthState = {
|
||||||
|
user: null,
|
||||||
|
session: null,
|
||||||
|
isLoading: true,
|
||||||
|
isAuthenticated: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authStore = store("auth", { ...defaultValue }, DEFAULT_STORE_CONFIG)
|
||||||
|
|
||||||
|
export const useAuthStore = () => {
|
||||||
|
void authStore.start
|
||||||
|
const state = useSnapshot(authStore.state)
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
store: authStore,
|
||||||
|
setUser,
|
||||||
|
setSession,
|
||||||
|
setLoading,
|
||||||
|
signOut,
|
||||||
|
checkSession,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setUser = (user: User | null) => {
|
||||||
|
authStore.state.user = user
|
||||||
|
authStore.state.isAuthenticated = !!user
|
||||||
|
}
|
||||||
|
|
||||||
|
const setSession = (session: Session | null) => {
|
||||||
|
authStore.state.session = session
|
||||||
|
authStore.state.user = session?.user ?? null
|
||||||
|
authStore.state.isAuthenticated = !!session
|
||||||
|
}
|
||||||
|
|
||||||
|
const setLoading = (isLoading: boolean) => {
|
||||||
|
authStore.state.isLoading = isLoading
|
||||||
|
}
|
||||||
|
|
||||||
|
const signOut = async () => {
|
||||||
|
const supabase = createClient()
|
||||||
|
await supabase.auth.signOut()
|
||||||
|
authStore.state.user = null
|
||||||
|
authStore.state.session = null
|
||||||
|
authStore.state.isAuthenticated = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkSession = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const supabase = createClient()
|
||||||
|
const {
|
||||||
|
data: { session },
|
||||||
|
} = await supabase.auth.getSession()
|
||||||
|
setSession(session)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error checking session:", error)
|
||||||
|
setSession(null)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
166
src/store/fps_test.ts
Normal file
166
src/store/fps_test.ts
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import { store } from "@tauri-store/valtio"
|
||||||
|
import { useSnapshot } from "valtio"
|
||||||
|
import { DEFAULT_STORE_CONFIG } from "./config"
|
||||||
|
import type { AllSystemInfo } from "tauri-plugin-system-info-api"
|
||||||
|
import type { VideoSetting } from "./tool"
|
||||||
|
|
||||||
|
export interface FpsTestResult {
|
||||||
|
id: string // 唯一标识符,使用时间戳
|
||||||
|
testTime: string // 测试时间(MM/DD HH:mm:ss)
|
||||||
|
testDate: string // 测试日期(ISO 格式,用于排序)
|
||||||
|
mapName: string // 测试地图名称
|
||||||
|
mapLabel: string // 测试地图标签
|
||||||
|
avg: number | null // 平均帧数
|
||||||
|
p1: number | null // P1 帧数(最低1%帧数)
|
||||||
|
rawResult: string // 原始测试结果
|
||||||
|
videoSetting: VideoSetting | null // 画面设置参数
|
||||||
|
hardwareInfo: {
|
||||||
|
cpu: string | null
|
||||||
|
cpuCount: number | null
|
||||||
|
os: string | null
|
||||||
|
memory: number | null // GB
|
||||||
|
memoryManufacturer: string | null
|
||||||
|
memorySpeed: number | null // MHz,实际频率 ConfiguredClockSpeed
|
||||||
|
memoryDefaultSpeed: number | null // MHz,默认频率 Speed(如果存在)
|
||||||
|
gpu: string | null
|
||||||
|
monitor: string | null
|
||||||
|
monitorManufacturer: string | null
|
||||||
|
monitorModel: string | null
|
||||||
|
motherboardModel: string | null // 合并后的制造商和型号
|
||||||
|
motherboardVersion: string | null
|
||||||
|
biosVersion: string | null
|
||||||
|
} | null // 硬件信息
|
||||||
|
note?: string // 备注(可选,用于向后兼容)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolutionGroupItem {
|
||||||
|
width: string
|
||||||
|
height: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValue = {
|
||||||
|
results: [] as FpsTestResult[],
|
||||||
|
// FpsTest配置数据
|
||||||
|
config: {
|
||||||
|
batchTestCount: 1, // 批量测试次数
|
||||||
|
isResolutionGroupEnabled: false, // 是否启用分辨率组
|
||||||
|
resolutionGroup: [] as ResolutionGroupItem[], // 分辨率组列表
|
||||||
|
testNote: "", // 测试备注
|
||||||
|
customLaunchOption: "", // 自定义启动项
|
||||||
|
isResolutionEnabled: true, // 是否启用分辨率和全屏设置
|
||||||
|
resolutionWidth: "", // 分辨率宽度
|
||||||
|
resolutionHeight: "", // 分辨率高度
|
||||||
|
isFullscreen: true, // 全屏模式
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fpsTestStore = store(
|
||||||
|
"fps_test",
|
||||||
|
{ ...defaultValue },
|
||||||
|
DEFAULT_STORE_CONFIG,
|
||||||
|
)
|
||||||
|
|
||||||
|
export const useFpsTestStore = () => {
|
||||||
|
void fpsTestStore.start
|
||||||
|
const state = useSnapshot(fpsTestStore.state)
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
store: fpsTestStore,
|
||||||
|
addResult,
|
||||||
|
removeResult,
|
||||||
|
clearResults,
|
||||||
|
updateNote,
|
||||||
|
// 配置相关方法
|
||||||
|
setBatchTestCount,
|
||||||
|
setIsResolutionGroupEnabled,
|
||||||
|
setResolutionGroup,
|
||||||
|
addResolutionToGroup,
|
||||||
|
removeResolutionFromGroup,
|
||||||
|
setTestNote,
|
||||||
|
setCustomLaunchOption,
|
||||||
|
setIsResolutionEnabled,
|
||||||
|
setResolution,
|
||||||
|
setIsFullscreen,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addResult = (result: FpsTestResult) => {
|
||||||
|
fpsTestStore.state.results = [result, ...fpsTestStore.state.results]
|
||||||
|
// 限制最多保存100条记录
|
||||||
|
if (fpsTestStore.state.results.length > 100) {
|
||||||
|
fpsTestStore.state.results = fpsTestStore.state.results.slice(0, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeResult = (id: string) => {
|
||||||
|
fpsTestStore.state.results = fpsTestStore.state.results.filter(
|
||||||
|
(r) => r.id !== id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearResults = () => {
|
||||||
|
fpsTestStore.state.results = []
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateNote = (id: string, note: string) => {
|
||||||
|
const result = fpsTestStore.state.results.find((r) => r.id === id)
|
||||||
|
if (result) {
|
||||||
|
result.note = note
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置相关方法
|
||||||
|
const setBatchTestCount = (count: number) => {
|
||||||
|
fpsTestStore.state.config.batchTestCount = count
|
||||||
|
}
|
||||||
|
|
||||||
|
const setIsResolutionGroupEnabled = (enabled: boolean) => {
|
||||||
|
fpsTestStore.state.config.isResolutionGroupEnabled = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
const setResolutionGroup = (group: ResolutionGroupItem[]) => {
|
||||||
|
fpsTestStore.state.config.resolutionGroup = group
|
||||||
|
}
|
||||||
|
|
||||||
|
const addResolutionToGroup = (resolution: ResolutionGroupItem) => {
|
||||||
|
// 检查是否已存在相同分辨率
|
||||||
|
const exists = fpsTestStore.state.config.resolutionGroup.some(
|
||||||
|
(r) => r.width === resolution.width && r.height === resolution.height
|
||||||
|
)
|
||||||
|
if (!exists) {
|
||||||
|
fpsTestStore.state.config.resolutionGroup = [
|
||||||
|
...fpsTestStore.state.config.resolutionGroup,
|
||||||
|
resolution,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeResolutionFromGroup = (index: number) => {
|
||||||
|
fpsTestStore.state.config.resolutionGroup = fpsTestStore.state.config.resolutionGroup.filter(
|
||||||
|
(_, i) => i !== index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setTestNote = (note: string) => {
|
||||||
|
fpsTestStore.state.config.testNote = note
|
||||||
|
}
|
||||||
|
|
||||||
|
const setCustomLaunchOption = (option: string) => {
|
||||||
|
fpsTestStore.state.config.customLaunchOption = option
|
||||||
|
}
|
||||||
|
|
||||||
|
const setIsResolutionEnabled = (enabled: boolean) => {
|
||||||
|
fpsTestStore.state.config.isResolutionEnabled = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
const setResolution = (width: string, height: string) => {
|
||||||
|
fpsTestStore.state.config.resolutionWidth = width
|
||||||
|
fpsTestStore.state.config.resolutionHeight = height
|
||||||
|
}
|
||||||
|
|
||||||
|
const setIsFullscreen = (isFullscreen: boolean) => {
|
||||||
|
fpsTestStore.state.config.isFullscreen = isFullscreen
|
||||||
|
}
|
||||||
|
|
||||||
153
src/store/hardware.ts
Normal file
153
src/store/hardware.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { store } from "@tauri-store/valtio"
|
||||||
|
import { useSnapshot } from "valtio"
|
||||||
|
import { DEFAULT_STORE_CONFIG } from "./config"
|
||||||
|
import { allSysInfo, type AllSystemInfo } from "tauri-plugin-system-info-api"
|
||||||
|
import { invoke } from "@tauri-apps/api/core"
|
||||||
|
|
||||||
|
export interface ComputerInfo {
|
||||||
|
OsName?: string
|
||||||
|
OSDisplayVersion?: string
|
||||||
|
BiosSMBIOSBIOSVersion?: string
|
||||||
|
CsManufacturer?: string
|
||||||
|
CsName?: string
|
||||||
|
ReleaseId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GpuInfo {
|
||||||
|
vendor: string
|
||||||
|
model: string
|
||||||
|
family: string
|
||||||
|
device_id: string
|
||||||
|
total_vram: number
|
||||||
|
used_vram: number
|
||||||
|
load_pct: number
|
||||||
|
temperature: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MemoryInfo {
|
||||||
|
capacity?: number // 容量(字节)
|
||||||
|
manufacturer?: string
|
||||||
|
speed?: number // MHz,实际频率 ConfiguredClockSpeed
|
||||||
|
default_speed?: number // MHz,默认频率 Speed(如果存在)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MonitorInfo {
|
||||||
|
manufacturer?: string
|
||||||
|
model?: string
|
||||||
|
name?: string
|
||||||
|
refresh_rate?: number // Hz
|
||||||
|
resolution_width?: number
|
||||||
|
resolution_height?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MotherboardInfo {
|
||||||
|
manufacturer?: string // 制造商
|
||||||
|
model?: string // 型号
|
||||||
|
version?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HardwareData {
|
||||||
|
allSysData: AllSystemInfo | null
|
||||||
|
computerInfo: ComputerInfo
|
||||||
|
gpuInfo: GpuInfo | null
|
||||||
|
memoryInfo: MemoryInfo[]
|
||||||
|
monitorInfo: MonitorInfo[]
|
||||||
|
motherboardInfo: MotherboardInfo | null
|
||||||
|
lastUpdated: number // 最后更新时间戳
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValue: HardwareData = {
|
||||||
|
allSysData: null,
|
||||||
|
computerInfo: {},
|
||||||
|
gpuInfo: null,
|
||||||
|
memoryInfo: [],
|
||||||
|
monitorInfo: [],
|
||||||
|
motherboardInfo: null,
|
||||||
|
lastUpdated: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 硬件信息 fetcher
|
||||||
|
const hardwareInfoFetcher = async (): Promise<HardwareData> => {
|
||||||
|
// 并行获取系统信息、PowerShell 信息、GPU 信息、内存信息、显示器信息和主板信息
|
||||||
|
const [sys, computerInfoData, gpuInfoData, memoryInfoData, monitorInfoData, motherboardInfoData] =
|
||||||
|
await Promise.all([
|
||||||
|
allSysInfo(),
|
||||||
|
invoke<ComputerInfo>("get_computer_info").catch((error) => {
|
||||||
|
console.error("获取 PowerShell 信息失败:", error)
|
||||||
|
return {} as ComputerInfo
|
||||||
|
}),
|
||||||
|
invoke<GpuInfo | null>("get_gpu_info").catch((error) => {
|
||||||
|
console.error("获取 GPU 信息失败:", error)
|
||||||
|
return null
|
||||||
|
}),
|
||||||
|
invoke<MemoryInfo[]>("get_memory_info").catch((error) => {
|
||||||
|
console.error("获取内存信息失败:", error)
|
||||||
|
return [] as MemoryInfo[]
|
||||||
|
}),
|
||||||
|
invoke<MonitorInfo[]>("get_monitor_info").catch((error) => {
|
||||||
|
console.error("获取显示器信息失败:", error)
|
||||||
|
return [] as MonitorInfo[]
|
||||||
|
}),
|
||||||
|
invoke<MotherboardInfo>("get_motherboard_info").catch((error) => {
|
||||||
|
console.error("获取主板信息失败:", error)
|
||||||
|
return { manufacturer: undefined, model: undefined, version: undefined } as MotherboardInfo
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
allSysData: sys,
|
||||||
|
computerInfo: computerInfoData,
|
||||||
|
gpuInfo: gpuInfoData,
|
||||||
|
memoryInfo: memoryInfoData,
|
||||||
|
monitorInfo: monitorInfoData,
|
||||||
|
motherboardInfo: motherboardInfoData,
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hardwareStore = store("hardware", { ...defaultValue }, DEFAULT_STORE_CONFIG)
|
||||||
|
|
||||||
|
// 检查数据是否过期(30分钟)
|
||||||
|
const isDataStale = (lastUpdated: number): boolean => {
|
||||||
|
const thirtyMinutes = 30 * 60 * 1000
|
||||||
|
return Date.now() - lastUpdated > thirtyMinutes
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取硬件信息(如果数据过期或不存在则重新获取)
|
||||||
|
export const fetchHardwareInfo = async (force = false): Promise<void> => {
|
||||||
|
// 如果数据存在且未过期,且不是强制刷新,则直接返回
|
||||||
|
if (!force && hardwareStore.state.allSysData && !isDataStale(hardwareStore.state.lastUpdated)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await hardwareInfoFetcher()
|
||||||
|
hardwareStore.state.allSysData = data.allSysData
|
||||||
|
hardwareStore.state.computerInfo = data.computerInfo
|
||||||
|
hardwareStore.state.gpuInfo = data.gpuInfo
|
||||||
|
hardwareStore.state.memoryInfo = data.memoryInfo
|
||||||
|
hardwareStore.state.monitorInfo = data.monitorInfo
|
||||||
|
hardwareStore.state.motherboardInfo = data.motherboardInfo
|
||||||
|
hardwareStore.state.lastUpdated = data.lastUpdated
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取硬件信息失败:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 强制刷新硬件信息
|
||||||
|
export const refreshHardwareInfo = async (): Promise<void> => {
|
||||||
|
await fetchHardwareInfo(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useHardwareStore = () => {
|
||||||
|
void hardwareStore.start
|
||||||
|
const state = useSnapshot(hardwareStore.state)
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
store: hardwareStore,
|
||||||
|
fetchHardwareInfo,
|
||||||
|
refreshHardwareInfo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
import { appConfigDir } from "@tauri-apps/api/path"
|
|
||||||
import { setStoreCollectionPath } from "@tauri-store/valtio"
|
|
||||||
import { appStore } from "./app"
|
import { appStore } from "./app"
|
||||||
|
import { authStore } from "./auth"
|
||||||
import { steamStore } from "./steam"
|
import { steamStore } from "./steam"
|
||||||
import { toolStore } from "./tool"
|
import { toolStore } from "./tool"
|
||||||
import path from "path"
|
import { fpsTestStore } from "./fps_test"
|
||||||
|
import { hardwareStore, fetchHardwareInfo } from "./hardware"
|
||||||
|
|
||||||
export async function init() {
|
export async function init() {
|
||||||
await appStore.start()
|
await appStore.start()
|
||||||
|
await authStore.start()
|
||||||
await toolStore.start()
|
await toolStore.start()
|
||||||
await steamStore.start()
|
await steamStore.start()
|
||||||
const appConfigDirPath = await appConfigDir()
|
await fpsTestStore.start()
|
||||||
await setStoreCollectionPath(path.resolve(appConfigDirPath, "cstb"))
|
await hardwareStore.start()
|
||||||
|
// 初始化时自动加载硬件信息(如果数据过期或不存在)
|
||||||
|
await fetchHardwareInfo()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,11 +74,20 @@ const setCs2DirChecking = (checking: boolean) => {
|
|||||||
|
|
||||||
const checkSteamDirValid = async () => {
|
const checkSteamDirValid = async () => {
|
||||||
setSteamDirChecking(true)
|
setSteamDirChecking(true)
|
||||||
const pathExist = await invoke<boolean>("check_path", { path: steamStore.state.steamDir })
|
try {
|
||||||
setSteamDirValid(pathExist)
|
// 使用专门的 Steam 路径验证,检查 steam.exe 或 config 目录
|
||||||
setTimeout(() => {
|
const isValid = await invoke<boolean>("check_steam_dir_valid", {
|
||||||
setSteamDirChecking(false)
|
steamDir: steamStore.state.steamDir
|
||||||
}, 500)
|
})
|
||||||
|
setSteamDirValid(isValid)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("验证 Steam 路径时出错:", error)
|
||||||
|
setSteamDirValid(false)
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => {
|
||||||
|
setSteamDirChecking(false)
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkCs2DirValid = async () => {
|
const checkCs2DirValid = async () => {
|
||||||
@@ -95,8 +104,21 @@ const currentUser = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getUsers = async () => {
|
const getUsers = async () => {
|
||||||
const users = await invoke<SteamUser[]>("get_steam_users", { steamDir: steamStore.state.steamDir })
|
// 只有在路径有效时才尝试获取用户
|
||||||
setUsers(users)
|
if (!steamStore.state.steamDirValid || !steamStore.state.steamDir) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const users = await invoke<SteamUser[]>("get_steam_users", {
|
||||||
|
steamDir: steamStore.state.steamDir
|
||||||
|
})
|
||||||
|
setUsers(users)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取 Steam 用户列表失败:", error)
|
||||||
|
// 如果获取失败,清空用户列表,避免显示错误数据
|
||||||
|
setUsers([])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectUser = (index: number) => {
|
const selectUser = (index: number) => {
|
||||||
|
|||||||
@@ -1,42 +1,162 @@
|
|||||||
import { store } from "@tauri-store/valtio"
|
import { store } from "@tauri-store/valtio"
|
||||||
import { useSnapshot } from "valtio"
|
import { useSnapshot } from "valtio"
|
||||||
import { DEFAULT_STORE_CONFIG } from "./config"
|
import { DEFAULT_STORE_CONFIG } from "./config"
|
||||||
|
import { emit } from "@tauri-apps/api/event"
|
||||||
|
import { invoke } from "@tauri-apps/api/core"
|
||||||
|
import VideoSetting from "@/components/cstb/VideoSetting"
|
||||||
|
|
||||||
interface LaunchOption {
|
export interface LaunchOption {
|
||||||
option: string
|
option: string
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VideoSetting {
|
export interface VideoSetting {
|
||||||
width: number; // 分辨率宽度
|
version: string; // 版本
|
||||||
height: number; // 分辨率高度
|
vendor_id: string; // 供应商ID
|
||||||
|
device_id: string; // 设备ID
|
||||||
|
cpu_level: string; // CPU等级
|
||||||
|
gpu_mem_level: string; // GPU内存等级
|
||||||
|
gpu_level: string; // GPU等级
|
||||||
|
knowndevice: string; // 已知设备
|
||||||
|
defaultres: string; // 默认分辨率宽度
|
||||||
|
defaultresheight: string; // 默认分辨率高度
|
||||||
|
refreshrate_numerator: string; // 刷新率分子
|
||||||
|
refreshrate_denominator: string; // 刷新率分母
|
||||||
fullscreen: string; // 全屏
|
fullscreen: string; // 全屏
|
||||||
vsync: string; // 垂直同步
|
coop_fullscreen: string; // 合作模式全屏
|
||||||
enhanceCharacterContrast: string; // 增强角色对比度
|
nowindowborder: string; // 无窗口边框
|
||||||
cmaa2AntiAliasing: string; // CMAA2抗锯齿
|
mat_vsync: string; // 垂直同步
|
||||||
msaaAntiAliasing: string; // 多重采样抗锯齿
|
fullscreen_min_on_focus_loss: string; // 失去焦点时最小化全屏
|
||||||
globalShadowQuality: string; // 全局阴影效果
|
high_dpi: string; // 高DPI
|
||||||
dynamicShadows: string; // 动态阴影
|
auto_config: string; // 自动配置
|
||||||
modelTextureDetail: string; // 模型/贴图细节
|
shaderquality: string; // 光影质量
|
||||||
textureFilteringMode: string; // 贴图过滤模式
|
r_texturefilteringquality: string; // 纹理过滤质量
|
||||||
lightShadowDetail: string; // 光影细节
|
msaa_samples: string; // 多重采样抗锯齿样本数
|
||||||
particleDetail: string; // 粒子细节
|
r_csgo_cmaa_enable: string; // CMAA抗锯齿启用
|
||||||
ambientOcclusion: string; // 环境光遮蔽
|
videocfg_shadow_quality: string; // 阴影质量
|
||||||
hdr: string; // 高动态范围
|
videocfg_dynamic_shadows: string; // 动态阴影
|
||||||
fidelityFxSuperResolution: string; // Fidelity FX 超级分辨率
|
videocfg_texture_detail: string; // 纹理细节
|
||||||
|
videocfg_particle_detail: string; // 粒子细节
|
||||||
|
videocfg_ao_detail: string; // 环境光遮蔽细节
|
||||||
|
videocfg_hdr_detail: string; // 高动态范围细节
|
||||||
|
videocfg_fsr_detail: string; // FSR细节
|
||||||
|
monitor_index: string; // 显示器索引
|
||||||
|
r_low_latency: string; // 低延迟
|
||||||
|
aspectratiomode: string; // 宽高比模式
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 视频设置预设模版
|
||||||
|
export const VideoSettingTemplate = {
|
||||||
|
veryhigh: {
|
||||||
|
shaderquality: "1",
|
||||||
|
r_texturefilteringquality: "3",
|
||||||
|
msaa_samples: "8",
|
||||||
|
r_csgo_cmaa_enable: "0",
|
||||||
|
videocfg_shadow_quality: "3",
|
||||||
|
videocfg_dynamic_shadows: "1",
|
||||||
|
videocfg_texture_detail: "2",
|
||||||
|
videocfg_particle_detail: "3",
|
||||||
|
videocfg_ao_detail: "3",
|
||||||
|
videocfg_hdr_detail: "-1",
|
||||||
|
videocfg_fsr_detail: "0",
|
||||||
|
},
|
||||||
|
high: {
|
||||||
|
shaderquality: "1",
|
||||||
|
r_texturefilteringquality: "3",
|
||||||
|
msaa_samples: "4",
|
||||||
|
r_csgo_cmaa_enable: "0",
|
||||||
|
videocfg_shadow_quality: "2",
|
||||||
|
videocfg_dynamic_shadows: "1",
|
||||||
|
videocfg_texture_detail: "2",
|
||||||
|
videocfg_particle_detail: "2",
|
||||||
|
videocfg_ao_detail: "2",
|
||||||
|
videocfg_hdr_detail: "-1",
|
||||||
|
videocfg_fsr_detail: "0",
|
||||||
|
},
|
||||||
|
middle: {
|
||||||
|
shaderquality: "0",
|
||||||
|
r_texturefilteringquality: "1",
|
||||||
|
msaa_samples: "2",
|
||||||
|
r_csgo_cmaa_enable: "0",
|
||||||
|
videocfg_shadow_quality: "1",
|
||||||
|
videocfg_dynamic_shadows: "1",
|
||||||
|
videocfg_texture_detail: "1",
|
||||||
|
videocfg_particle_detail: "1",
|
||||||
|
videocfg_ao_detail: "0",
|
||||||
|
videocfg_fsr_detail: "2",
|
||||||
|
},
|
||||||
|
low: {
|
||||||
|
shaderquality: "0",
|
||||||
|
r_texturefilteringquality: "0",
|
||||||
|
msaa_samples: "0",
|
||||||
|
r_csgo_cmaa_enable: "0",
|
||||||
|
videocfg_shadow_quality: "0",
|
||||||
|
videocfg_dynamic_shadows: "0",
|
||||||
|
videocfg_texture_detail: "0",
|
||||||
|
videocfg_particle_detail: "0",
|
||||||
|
videocfg_ao_detail: "0",
|
||||||
|
videocfg_hdr_detail: "3",
|
||||||
|
videocfg_fsr_detail: "3",
|
||||||
|
},
|
||||||
|
recommend: {
|
||||||
|
shaderquality: "0",
|
||||||
|
r_texturefilteringquality: "3",
|
||||||
|
msaa_samples: "2",
|
||||||
|
r_csgo_cmaa_enable: "0",
|
||||||
|
videocfg_shadow_quality: "0",
|
||||||
|
videocfg_dynamic_shadows: "1",
|
||||||
|
videocfg_texture_detail: "1",
|
||||||
|
videocfg_particle_detail: "0",
|
||||||
|
videocfg_ao_detail: "0",
|
||||||
|
videocfg_hdr_detail: "3",
|
||||||
|
videocfg_fsr_detail: "0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const defaultValue = {
|
const defaultValue = {
|
||||||
launchOptions: [
|
launchOptions: [
|
||||||
{ option: "-novid -high -freq 144 -fullscreen", name: "" },
|
{ option: "-novid -high -freq 240 -coop_fullscreen -allow_third_party_software", name: "游戏" },
|
||||||
{ option: "-novid -high -w 1920 -h 1080 -freq 144 -sw -noborder", name: "" },
|
{ option: "-novid -high -w 1920 -h 1080 -freq 240 -sw -noborder -allow_third_party_software", name: "录像" },
|
||||||
{ option: "-novid -high -freq 144 -fullscreen -allow_third_party_software", name: "" },
|
{ option: "-novid -high -freq 240 -fullscreen -allow_third_party_software", name: "测试" },
|
||||||
] as LaunchOption[],
|
] as LaunchOption[],
|
||||||
launchIndex: 0,
|
launchIndex: 0,
|
||||||
powerPlan: 0,
|
powerPlan: 0,
|
||||||
|
autoCloseGame: true, // 帧数测试自动关闭游戏
|
||||||
|
isGameRunning: false, // 游戏运行状态(全局共享)
|
||||||
videoSetting: {
|
videoSetting: {
|
||||||
width: 1920,
|
version: "15",
|
||||||
height: 1080
|
vendor_id: "0",
|
||||||
|
device_id: "0",
|
||||||
|
cpu_level: "3",
|
||||||
|
gpu_mem_level: "3",
|
||||||
|
gpu_level: "3",
|
||||||
|
knowndevice: "0",
|
||||||
|
defaultres: "1920",
|
||||||
|
defaultresheight: "1080",
|
||||||
|
refreshrate_numerator: "144",
|
||||||
|
refreshrate_denominator: "1",
|
||||||
|
fullscreen: "1",
|
||||||
|
coop_fullscreen: "0",
|
||||||
|
nowindowborder: "1",
|
||||||
|
mat_vsync: "0",
|
||||||
|
fullscreen_min_on_focus_loss: "1",
|
||||||
|
high_dpi: "0",
|
||||||
|
auto_config: "2",
|
||||||
|
shaderquality: "0",
|
||||||
|
r_texturefilteringquality: "3",
|
||||||
|
msaa_samples: "2",
|
||||||
|
r_csgo_cmaa_enable: "0",
|
||||||
|
videocfg_shadow_quality: "0",
|
||||||
|
videocfg_dynamic_shadows: "1",
|
||||||
|
videocfg_texture_detail: "1",
|
||||||
|
videocfg_particle_detail: "0",
|
||||||
|
videocfg_ao_detail: "0",
|
||||||
|
videocfg_hdr_detail: "3",
|
||||||
|
videocfg_fsr_detail: "0",
|
||||||
|
monitor_index: "0",
|
||||||
|
r_low_latency: "1",
|
||||||
|
aspectratiomode: "0",
|
||||||
} as VideoSetting,
|
} as VideoSetting,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,16 +170,29 @@ export const useToolStore = () => {
|
|||||||
void toolStore.start
|
void toolStore.start
|
||||||
const state = useSnapshot(toolStore.state)
|
const state = useSnapshot(toolStore.state)
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
setTimeout(() => {
|
||||||
|
sendCurrentLaunchOptionToTray(state.launchIndex)
|
||||||
|
sendPowerPlanToTray(state.powerPlan)
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
state,
|
state,
|
||||||
store: toolStore,
|
store: toolStore,
|
||||||
setLaunchOption,
|
setLaunchOption,
|
||||||
setLaunchOptions,
|
setLaunchOptions,
|
||||||
setLaunchIndex,
|
setLaunchIndex,
|
||||||
|
removeLaunchOption,
|
||||||
setPowerPlan,
|
setPowerPlan,
|
||||||
|
setAutoCloseGame,
|
||||||
setVideoSetting,
|
setVideoSetting,
|
||||||
|
getVideoConfig,
|
||||||
|
setVideoConfig,
|
||||||
addLaunchOption,
|
addLaunchOption,
|
||||||
resetToolStore,
|
resetToolStore,
|
||||||
|
setIsGameRunning,
|
||||||
|
checkGameRunning,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,35 +202,116 @@ const setLaunchOption = (option: LaunchOption, index: number) => {
|
|||||||
option,
|
option,
|
||||||
...toolStore.state.launchOptions.slice(index + 1),
|
...toolStore.state.launchOptions.slice(index + 1),
|
||||||
]
|
]
|
||||||
|
// 同步更新托盘
|
||||||
|
sendCurrentLaunchOptionToTray(toolStore.state.launchIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
const setLaunchOptions = (options: LaunchOption[]) => {
|
const setLaunchOptions = (options: LaunchOption[]) => {
|
||||||
toolStore.state.launchOptions = options
|
toolStore.state.launchOptions = options
|
||||||
|
// 确保索引在有效范围内
|
||||||
|
if (toolStore.state.launchIndex >= options.length) {
|
||||||
|
toolStore.state.launchIndex = options.length > 0 ? options.length - 1 : 0
|
||||||
|
}
|
||||||
|
sendCurrentLaunchOptionToTray(toolStore.state.launchIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
const setLaunchIndex = (index: number) => {
|
const setLaunchIndex = (index: number) => {
|
||||||
toolStore.state.launchIndex = index
|
toolStore.state.launchIndex = index
|
||||||
|
sendCurrentLaunchOptionToTray(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const removeLaunchOption = (index: number) => {
|
||||||
|
toolStore.state.launchOptions = [
|
||||||
|
...toolStore.state.launchOptions.slice(0, index),
|
||||||
|
...toolStore.state.launchOptions.slice(index + 1),
|
||||||
|
]
|
||||||
|
// 如果删除的是当前项或当前项在删除项之后,需要调整索引
|
||||||
|
if (index <= toolStore.state.launchIndex) {
|
||||||
|
if (toolStore.state.launchIndex > 0) {
|
||||||
|
toolStore.state.launchIndex -= 1
|
||||||
|
} else {
|
||||||
|
toolStore.state.launchIndex = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 确保索引在有效范围内
|
||||||
|
if (toolStore.state.launchIndex >= toolStore.state.launchOptions.length) {
|
||||||
|
toolStore.state.launchIndex = toolStore.state.launchOptions.length > 0 ? toolStore.state.launchOptions.length - 1 : 0
|
||||||
|
}
|
||||||
|
sendCurrentLaunchOptionToTray(toolStore.state.launchIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendCurrentLaunchOptionToTray = (index: number) => {
|
||||||
|
// 发送完整的启动项列表和当前索引到托盘
|
||||||
|
void emit("tray://update_launch_options", {
|
||||||
|
options: toolStore.state.launchOptions,
|
||||||
|
currentIndex: index,
|
||||||
|
})
|
||||||
|
}
|
||||||
const setPowerPlan = (plan: number) => {
|
const setPowerPlan = (plan: number) => {
|
||||||
toolStore.state.powerPlan = plan
|
toolStore.state.powerPlan = plan
|
||||||
|
sendPowerPlanToTray(plan)
|
||||||
|
}
|
||||||
|
const sendPowerPlanToTray = (plan: number) => {
|
||||||
|
void emit("tray://get_powerplan", plan)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setAutoCloseGame = (enabled: boolean) => {
|
||||||
|
toolStore.state.autoCloseGame = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
const setVideoSetting = (setting: VideoSetting) => {
|
const setVideoSetting = (setting: VideoSetting) => {
|
||||||
toolStore.state.videoSetting = setting
|
toolStore.state.videoSetting = setting
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getVideoConfig = async (steam_dir: string, steam_id32: number) => {
|
||||||
|
try {
|
||||||
|
const video = await invoke<VideoSetting>("get_cs2_video_config", { steamDir: steam_dir, steamId32: steam_id32 })
|
||||||
|
// console.log(video)
|
||||||
|
setVideoSetting(video)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("读取视频配置失败:", error)
|
||||||
|
// 如果文件不存在或读取失败,使用默认配置或保持当前配置
|
||||||
|
// 不抛出错误,避免影响其他功能
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setVideoConfig = async (steam_dir: string, steam_id32: number, video_config: VideoSetting) => {
|
||||||
|
// console.log(video_config.videocfg_hdr_detail)
|
||||||
|
await invoke("set_cs2_video_config", { steamDir: steam_dir, steamId32: steam_id32, videoConfig: video_config })
|
||||||
|
}
|
||||||
|
|
||||||
const addLaunchOption = (option: LaunchOption) => {
|
const addLaunchOption = (option: LaunchOption) => {
|
||||||
// 限制最高10个
|
// 限制最高10个
|
||||||
if (toolStore.state.launchOptions.length >= 10) {
|
if (toolStore.state.launchOptions.length >= 10) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
toolStore.state.launchOptions = [...toolStore.state.launchOptions, option]
|
toolStore.state.launchOptions = [...toolStore.state.launchOptions, option]
|
||||||
|
// 同步更新托盘
|
||||||
|
sendCurrentLaunchOptionToTray(toolStore.state.launchIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setIsGameRunning = (running: boolean) => {
|
||||||
|
toolStore.state.isGameRunning = running
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkGameRunning = async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const result = await invoke<boolean>("check_process_running", {
|
||||||
|
processName: "cs2.exe",
|
||||||
|
}).catch(() => false)
|
||||||
|
setIsGameRunning(result)
|
||||||
|
return result
|
||||||
|
} catch {
|
||||||
|
setIsGameRunning(false)
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetToolStore = () => {
|
const resetToolStore = () => {
|
||||||
setLaunchOptions(defaultValue.launchOptions)
|
setLaunchOptions(defaultValue.launchOptions)
|
||||||
setLaunchIndex(defaultValue.launchIndex)
|
setLaunchIndex(defaultValue.launchIndex)
|
||||||
setPowerPlan(defaultValue.powerPlan)
|
setPowerPlan(defaultValue.powerPlan)
|
||||||
|
setAutoCloseGame(defaultValue.autoCloseGame)
|
||||||
setVideoSetting(defaultValue.videoSetting)
|
setVideoSetting(defaultValue.videoSetting)
|
||||||
|
setIsGameRunning(defaultValue.isGameRunning)
|
||||||
}
|
}
|
||||||
|
|||||||
92
src/utils/auth.ts
Normal file
92
src/utils/auth.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { open } from "@tauri-apps/plugin-shell"
|
||||||
|
import { createClient } from "@/utils/supabase/client"
|
||||||
|
import type { Session } from "@supabase/supabase-js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开网页端登录页面
|
||||||
|
*/
|
||||||
|
export async function openLoginPage() {
|
||||||
|
const loginUrl = "https://cstb.upup.cool/auth/login?redirect=cstb://auth"
|
||||||
|
await open(loginUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开网页端注册页面
|
||||||
|
*/
|
||||||
|
export async function openSignupPage() {
|
||||||
|
const signupUrl = "https://cstb.upup.cool/auth/signup?redirect=cstb://auth"
|
||||||
|
await open(signupUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 deep-link 回调中的认证信息
|
||||||
|
* @param url - deep-link URL,格式: cstb://auth?access_token=xxx&refresh_token=xxx 或 cstb://auth?session=xxx
|
||||||
|
*/
|
||||||
|
export async function handleAuthCallback(url: string): Promise<Session | null> {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url)
|
||||||
|
const params = new URLSearchParams(urlObj.search)
|
||||||
|
|
||||||
|
// 方式1: 从 URL 参数中获取 access_token 和 refresh_token
|
||||||
|
const accessToken = params.get("access_token")
|
||||||
|
const refreshToken = params.get("refresh_token")
|
||||||
|
|
||||||
|
if (accessToken && refreshToken) {
|
||||||
|
const supabase = createClient()
|
||||||
|
const { data, error } = await supabase.auth.setSession({
|
||||||
|
access_token: accessToken,
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Error setting session:", error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.session
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方式2: 从 session 参数中获取(如果网页端返回的是完整 session)
|
||||||
|
const sessionParam = params.get("session")
|
||||||
|
if (sessionParam) {
|
||||||
|
try {
|
||||||
|
const session = JSON.parse(decodeURIComponent(sessionParam)) as Session
|
||||||
|
const supabase = createClient()
|
||||||
|
const { data, error } = await supabase.auth.setSession({
|
||||||
|
access_token: session.access_token,
|
||||||
|
refresh_token: session.refresh_token,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Error setting session:", error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.session
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error parsing session:", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方式3: 如果网页端通过 code 参数返回(PKCE flow)
|
||||||
|
const code = params.get("code")
|
||||||
|
if (code) {
|
||||||
|
const supabase = createClient()
|
||||||
|
const { data, error } = await supabase.auth.exchangeCodeForSession(code)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Error exchanging code for session:", error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.session
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error handling auth callback:", error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -8,7 +8,6 @@ module.exports = {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
plugins: [],
|
|
||||||
darkMode: "class",
|
darkMode: "class",
|
||||||
plugins: [heroui()],
|
plugins: [heroui()],
|
||||||
}
|
}
|
||||||
|
|||||||
286
todo/refactor-plan.md
Normal file
286
todo/refactor-plan.md
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
# 代码重构规划文档
|
||||||
|
|
||||||
|
## 统计结果
|
||||||
|
|
||||||
|
### 超过300行的文件列表
|
||||||
|
|
||||||
|
| 文件路径 | 行数 | 优先级 | 状态 |
|
||||||
|
|---------|------|--------|------|
|
||||||
|
| `src/components/cstb/FpsTest.tsx` | 1949 | 🔴 高 | 待重构 |
|
||||||
|
| `src/components/cstb/VideoSetting.tsx` | 579 | 🟡 中 | 待重构 |
|
||||||
|
| `src/components/cstb/SteamUsers.tsx` | 432 | 🟡 中 | 待重构 |
|
||||||
|
|
||||||
|
## 重构目标
|
||||||
|
|
||||||
|
1. **组件封装**:将大组件拆分为更小的、职责单一的组件
|
||||||
|
2. **功能复用**:提取可复用的逻辑和工具函数
|
||||||
|
3. **文件拆分**:将大文件拆分为多个小文件,提高可维护性
|
||||||
|
4. **代码组织**:按功能模块组织代码结构
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. FpsTest.tsx (1949行) - 高优先级
|
||||||
|
|
||||||
|
### 问题分析
|
||||||
|
- 文件过大,难以维护和调试
|
||||||
|
- 包含多个职责:测试执行、结果读取、UI渲染、CSV导出等
|
||||||
|
- 大量状态管理和副作用逻辑混在一起
|
||||||
|
- 工具函数、常量、组件定义都在同一文件
|
||||||
|
|
||||||
|
### 重构方案
|
||||||
|
|
||||||
|
#### 1.1 文件结构拆分
|
||||||
|
```
|
||||||
|
src/components/cstb/FpsTest/
|
||||||
|
├── index.tsx # 主组件(精简后约200行)
|
||||||
|
├── types.ts # 类型定义
|
||||||
|
├── constants.ts # 常量定义(BENCHMARK_MAPS, PRESET_RESOLUTIONS等)
|
||||||
|
├── utils/
|
||||||
|
│ ├── vprof-parser.ts # VProf报告解析工具
|
||||||
|
│ ├── timestamp.ts # 时间戳处理工具
|
||||||
|
│ ├── fps-metrics.ts # FPS指标提取工具
|
||||||
|
│ └── csv-export.ts # CSV导出工具
|
||||||
|
├── hooks/
|
||||||
|
│ ├── useGameMonitor.ts # 游戏运行状态监控
|
||||||
|
│ ├── useTestMonitor.ts # 测试结果监控
|
||||||
|
│ ├── useBatchTest.ts # 批量测试逻辑
|
||||||
|
│ └── useHardwareInfo.ts # 硬件信息获取
|
||||||
|
├── components/
|
||||||
|
│ ├── TestConfigPanel.tsx # 测试配置面板
|
||||||
|
│ ├── ResolutionConfig.tsx # 分辨率配置组件
|
||||||
|
│ ├── TestResultsTable.tsx # 测试结果表格
|
||||||
|
│ ├── TestResultDisplay.tsx # 测试结果展示
|
||||||
|
│ ├── BatchTestProgress.tsx # 批量测试进度
|
||||||
|
│ └── NoteCell.tsx # 备注单元格
|
||||||
|
└── services/
|
||||||
|
├── testRunner.ts # 测试执行服务
|
||||||
|
└── resultReader.ts # 结果读取服务
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.2 功能模块拆分
|
||||||
|
|
||||||
|
**工具函数模块 (utils/)**
|
||||||
|
- `vprof-parser.ts`: `parseVProfReport`, `extractFpsMetrics`
|
||||||
|
- `timestamp.ts`: `compareTimestamps`, 时间戳格式化
|
||||||
|
- `fps-metrics.ts`: FPS相关计算和格式化
|
||||||
|
- `csv-export.ts`: `handleExportCSV`, `handleExportAverageCSV`
|
||||||
|
|
||||||
|
**自定义Hooks (hooks/)**
|
||||||
|
- `useGameMonitor.ts`: 游戏运行状态检测和监控
|
||||||
|
- `useTestMonitor.ts`: 测试结果文件监控逻辑
|
||||||
|
- `useBatchTest.ts`: 批量测试状态管理和执行逻辑
|
||||||
|
- `useHardwareInfo.ts`: 硬件信息获取和管理
|
||||||
|
|
||||||
|
**组件拆分 (components/)**
|
||||||
|
- `TestConfigPanel.tsx`: 测试地图、批量测试次数、备注配置
|
||||||
|
- `ResolutionConfig.tsx`: 分辨率设置、全屏/窗口化、分辨率组管理
|
||||||
|
- `TestResultsTable.tsx`: 测试结果表格展示(包含删除、编辑备注)
|
||||||
|
- `TestResultDisplay.tsx`: 当前测试结果显示(时间戳、avg、p1)
|
||||||
|
- `BatchTestProgress.tsx`: 批量测试进度显示组件
|
||||||
|
- `NoteCell.tsx`: 备注单元格组件
|
||||||
|
|
||||||
|
**服务层 (services/)**
|
||||||
|
- `testRunner.ts`: `runSingleTest`, `startTest` 等测试执行逻辑
|
||||||
|
- `resultReader.ts`: `readResult` 结果读取逻辑
|
||||||
|
|
||||||
|
#### 1.3 预期效果
|
||||||
|
- 主组件文件从1949行减少到约200行
|
||||||
|
- 每个子文件控制在100-300行以内
|
||||||
|
- 提高代码可维护性和可测试性
|
||||||
|
- 便于功能扩展和bug修复
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. VideoSetting.tsx (579行) - 中优先级
|
||||||
|
|
||||||
|
### 问题分析
|
||||||
|
- 视频设置配置数组过长(约200行)
|
||||||
|
- 编辑/只读模式切换逻辑复杂
|
||||||
|
- 文件监听和自动刷新逻辑可以提取
|
||||||
|
|
||||||
|
### 重构方案
|
||||||
|
|
||||||
|
#### 2.1 文件结构拆分
|
||||||
|
```
|
||||||
|
src/components/cstb/VideoSetting/
|
||||||
|
├── index.tsx # 主组件(精简后约200行)
|
||||||
|
├── types.ts # 类型定义
|
||||||
|
├── config/
|
||||||
|
│ └── videoSettingsConfig.ts # 视频设置配置数组
|
||||||
|
├── hooks/
|
||||||
|
│ ├── useVideoConfig.ts # 视频配置读取/写入
|
||||||
|
│ ├── useFileWatcher.ts # 文件监听逻辑
|
||||||
|
│ └── useGameRunning.ts # 游戏运行状态检测
|
||||||
|
└── components/
|
||||||
|
├── VideoSettingsEditor.tsx # 编辑模式组件
|
||||||
|
└── VideoSettingsViewer.tsx # 只读模式组件
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2 功能模块拆分
|
||||||
|
|
||||||
|
**配置模块 (config/)**
|
||||||
|
- `videoSettingsConfig.ts`: 提取 `videoSettings` 函数和配置数组
|
||||||
|
|
||||||
|
**自定义Hooks (hooks/)**
|
||||||
|
- `useVideoConfig.ts`: 视频配置的读取、写入、刷新逻辑
|
||||||
|
- `useFileWatcher.ts`: 文件变动监听和自动刷新
|
||||||
|
- `useGameRunning.ts`: 游戏运行状态检测(可复用)
|
||||||
|
|
||||||
|
**组件拆分 (components/)**
|
||||||
|
- `VideoSettingsEditor.tsx`: 编辑模式下的所有控件
|
||||||
|
- `VideoSettingsViewer.tsx`: 只读模式下的展示
|
||||||
|
|
||||||
|
#### 2.3 预期效果
|
||||||
|
- 主组件文件从579行减少到约200行
|
||||||
|
- 配置数据独立管理,便于维护
|
||||||
|
- 编辑/查看逻辑分离,代码更清晰
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. SteamUsers.tsx (432行) - 中优先级
|
||||||
|
|
||||||
|
### 问题分析
|
||||||
|
- 三种视图模式的渲染逻辑重复
|
||||||
|
- 用户项渲染函数较长
|
||||||
|
- 可以提取为独立组件
|
||||||
|
|
||||||
|
### 重构方案
|
||||||
|
|
||||||
|
#### 3.1 文件结构拆分
|
||||||
|
```
|
||||||
|
src/components/cstb/SteamUsers/
|
||||||
|
├── index.tsx # 主组件(精简后约150行)
|
||||||
|
├── types.ts # 类型定义
|
||||||
|
├── components/
|
||||||
|
│ ├── UserCard.tsx # 卡片视图用户项
|
||||||
|
│ ├── UserListItem.tsx # 列表视图用户项
|
||||||
|
│ └── UserListLargeItem.tsx # 大列表视图用户项
|
||||||
|
└── hooks/
|
||||||
|
└── useSteamUsers.ts # 用户数据获取和管理
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2 功能模块拆分
|
||||||
|
|
||||||
|
**组件拆分 (components/)**
|
||||||
|
- `UserCard.tsx`: 卡片样式用户项组件
|
||||||
|
- `UserListItem.tsx`: 列表样式用户项组件
|
||||||
|
- `UserListLargeItem.tsx`: 大列表样式用户项组件
|
||||||
|
|
||||||
|
**自定义Hooks (hooks/)**
|
||||||
|
- `useSteamUsers.ts`: 用户数据获取、刷新、模拟数据逻辑
|
||||||
|
|
||||||
|
#### 3.3 预期效果
|
||||||
|
- 主组件文件从432行减少到约150行
|
||||||
|
- 每种视图模式独立组件,便于维护
|
||||||
|
- 减少代码重复
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 重构执行计划
|
||||||
|
|
||||||
|
### 阶段1: FpsTest.tsx 重构(最高优先级)
|
||||||
|
1. ✅ 创建目录结构和基础文件
|
||||||
|
2. ✅ 提取工具函数到 `utils/` 目录
|
||||||
|
- ✅ `vprof-parser.ts` - VProf报告解析
|
||||||
|
- ✅ `timestamp.ts` - 时间戳处理
|
||||||
|
- ✅ `fps-metrics.ts` - FPS指标提取
|
||||||
|
- ✅ `csv-export.ts` - CSV导出功能
|
||||||
|
3. ✅ 提取自定义Hooks到 `hooks/` 目录
|
||||||
|
- ✅ `useGameMonitor.ts` - 游戏运行状态监控
|
||||||
|
- ✅ `useHardwareInfo.ts` - 硬件信息获取
|
||||||
|
- ⏳ `useTestMonitor.ts` - 测试结果监控(待完成)
|
||||||
|
- ⏳ `useBatchTest.ts` - 批量测试逻辑(待完成)
|
||||||
|
4. ✅ 提取部分UI组件到 `components/` 目录
|
||||||
|
- ✅ `NoteCell.tsx` - 备注单元格组件
|
||||||
|
- ⏳ `TestConfigPanel.tsx` - 测试配置面板(待完成)
|
||||||
|
- ⏳ `ResolutionConfig.tsx` - 分辨率配置(待完成)
|
||||||
|
- ⏳ `TestResultsTable.tsx` - 测试结果表格(待完成)
|
||||||
|
- ⏳ `TestResultDisplay.tsx` - 测试结果展示(待完成)
|
||||||
|
- ⏳ `BatchTestProgress.tsx` - 批量测试进度(待完成)
|
||||||
|
5. ⏳ 提取服务层逻辑到 `services/` 目录
|
||||||
|
- ⏳ `testRunner.ts` - 测试执行服务(待完成)
|
||||||
|
- ⏳ `resultReader.ts` - 结果读取服务(待完成)
|
||||||
|
6. ⏳ 重构主组件,整合所有子模块(进行中)
|
||||||
|
- ✅ 创建类型定义文件
|
||||||
|
- ⏳ 完整重构主组件(需要继续)
|
||||||
|
7. ⏳ 测试验证功能完整性(待完成)
|
||||||
|
|
||||||
|
### 阶段2: VideoSetting.tsx 重构
|
||||||
|
1. ⏳ 提取配置数据
|
||||||
|
2. ⏳ 提取自定义Hooks
|
||||||
|
3. ⏳ 拆分编辑/查看组件
|
||||||
|
4. ⏳ 重构主组件
|
||||||
|
|
||||||
|
### 阶段3: SteamUsers.tsx 重构
|
||||||
|
1. ⏳ 提取用户项组件
|
||||||
|
2. ⏳ 提取数据管理Hook
|
||||||
|
3. ⏳ 重构主组件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 重构原则
|
||||||
|
|
||||||
|
1. **保持功能完整性**:重构过程中确保所有功能正常工作
|
||||||
|
2. **渐进式重构**:分步骤进行,每步完成后验证
|
||||||
|
3. **向后兼容**:不改变对外接口,保持组件使用方式不变
|
||||||
|
4. **代码复用**:提取公共逻辑,避免重复代码
|
||||||
|
5. **类型安全**:保持TypeScript类型完整性
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 已完成的工作
|
||||||
|
|
||||||
|
### FpsTest.tsx 重构进度
|
||||||
|
|
||||||
|
#### ✅ 已完成
|
||||||
|
1. **目录结构创建**
|
||||||
|
- ✅ `src/components/cstb/FpsTest/` 目录结构已创建
|
||||||
|
|
||||||
|
2. **常量提取**
|
||||||
|
- ✅ `constants.ts` - BENCHMARK_MAPS, PRESET_RESOLUTIONS, TEST_TIMEOUT
|
||||||
|
|
||||||
|
3. **工具函数提取**
|
||||||
|
- ✅ `utils/vprof-parser.ts` - parseVProfReport函数
|
||||||
|
- ✅ `utils/timestamp.ts` - compareTimestamps, formatCurrentTimestamp, timestampToISO
|
||||||
|
- ✅ `utils/fps-metrics.ts` - extractFpsMetrics函数
|
||||||
|
- ✅ `utils/csv-export.ts` - handleExportCSV, handleExportAverageCSV, formatVideoSettingSummary
|
||||||
|
|
||||||
|
4. **自定义Hooks提取**
|
||||||
|
- ✅ `hooks/useGameMonitor.ts` - 游戏运行状态监控
|
||||||
|
- ✅ `hooks/useHardwareInfo.ts` - 硬件信息获取
|
||||||
|
|
||||||
|
5. **组件提取**
|
||||||
|
- ✅ `components/NoteCell.tsx` - 备注单元格组件
|
||||||
|
|
||||||
|
6. **类型定义**
|
||||||
|
- ✅ `types.ts` - Resolution, BatchTestProgress, FpsMetrics, ResolutionGroupInfo
|
||||||
|
|
||||||
|
#### ⏳ 待完成
|
||||||
|
1. **更多Hooks**
|
||||||
|
- `hooks/useTestMonitor.ts` - 测试结果文件监控逻辑
|
||||||
|
- `hooks/useBatchTest.ts` - 批量测试状态管理和执行逻辑
|
||||||
|
|
||||||
|
2. **更多组件**
|
||||||
|
- `components/TestConfigPanel.tsx` - 测试地图、批量测试次数、备注配置
|
||||||
|
- `components/ResolutionConfig.tsx` - 分辨率设置组件
|
||||||
|
- `components/TestResultsTable.tsx` - 测试结果表格
|
||||||
|
- `components/TestResultDisplay.tsx` - 当前测试结果显示
|
||||||
|
- `components/BatchTestProgress.tsx` - 批量测试进度显示
|
||||||
|
|
||||||
|
3. **服务层**
|
||||||
|
- `services/testRunner.ts` - 测试执行服务(runSingleTest, startTest)
|
||||||
|
- `services/resultReader.ts` - 结果读取服务(readResult)
|
||||||
|
|
||||||
|
4. **主组件重构**
|
||||||
|
- 使用提取的模块重构 `index.tsx`
|
||||||
|
- 确保所有功能正常工作
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 重构前确保有完整的测试覆盖(如果有)
|
||||||
|
2. 重构过程中注意保持状态管理的一致性
|
||||||
|
3. 提取Hook时注意依赖项的正确传递
|
||||||
|
4. 组件拆分时注意props的类型定义
|
||||||
|
5. 工具函数提取时注意副作用处理
|
||||||
|
6. **重要**: 原文件 `FpsTest.tsx` 暂时保留,待重构完成并测试通过后再删除
|
||||||
|
7. **当前状态**: 已提取基础模块,主组件重构需要继续完成
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "react-jsx",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
@@ -35,7 +35,8 @@
|
|||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.tsx",
|
"**/*.tsx",
|
||||||
"*.js",
|
"*.js",
|
||||||
".next/types/**/*.ts"
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
|
|||||||
Reference in New Issue
Block a user