[fix] update setup
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -42,4 +42,6 @@ yarn-error.log*
|
|||||||
|
|
||||||
temp/
|
temp/
|
||||||
src-tauri/temp/
|
src-tauri/temp/
|
||||||
log/
|
log/
|
||||||
|
.tauri/
|
||||||
|
todo/
|
||||||
13
latest.json
Normal file
13
latest.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": "0.0.6-beta.6",
|
||||||
|
"notes": "版本 0.0.6-beta.6 更新",
|
||||||
|
"pub_date": "2025-11-08T14:22:43.761Z",
|
||||||
|
"platforms": {
|
||||||
|
"windows-x86_64": {
|
||||||
|
"url": "https://github.com/plsgo/cstb/releases/download/v0.0.6-beta.6/CS工具箱_0.0.6-beta.6_x64-setup.exe",
|
||||||
|
"signature": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"download_url": "https://github.com/plsgo/cstb/releases/download/v0.0.6-beta.6/CS工具箱_0.0.6-beta.6_x64-setup.exe",
|
||||||
|
"signature": null
|
||||||
|
}
|
||||||
@@ -15,7 +15,10 @@
|
|||||||
"build-fast-prod": "tauri build -b nsis -- --profile fast-release",
|
"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",
|
||||||
|
"gen:debug": "node scripts/generate-latest-json.js --base-url https://github.com/plsgo/cstb/releases --target debug",
|
||||||
|
"gen:fast-release": "node scripts/generate-latest-json.js --base-url https://github.com/plsgo/cstb/releases --target fast-release"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formkit/auto-animate": "^0.8.4",
|
"@formkit/auto-animate": "^0.8.4",
|
||||||
@@ -37,6 +40,7 @@
|
|||||||
"@tauri-apps/plugin-process": "^2.3.1",
|
"@tauri-apps/plugin-process": "^2.3.1",
|
||||||
"@tauri-apps/plugin-shell": "^2.3.3",
|
"@tauri-apps/plugin-shell": "^2.3.3",
|
||||||
"@tauri-apps/plugin-store": "^2.4.1",
|
"@tauri-apps/plugin-store": "^2.4.1",
|
||||||
|
"@tauri-apps/plugin-updater": "^2.9.0",
|
||||||
"@tauri-store/valtio": "^3.2.0",
|
"@tauri-store/valtio": "^3.2.0",
|
||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"ahooks": "^3.9.6",
|
"ahooks": "^3.9.6",
|
||||||
|
|||||||
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` 参数匹配。
|
||||||
|
|
||||||
275
scripts/generate-latest-json.js
Normal file
275
scripts/generate-latest-json.js
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
#!/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)
|
||||||
|
*/
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
// 验证 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保 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) {
|
||||||
|
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}/${filename}`;
|
||||||
|
}
|
||||||
|
// 普通 URL
|
||||||
|
return `${cleanBaseUrl}/${filename}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据 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) {
|
||||||
|
console.log(` ✓ 找到文件: ${result.file}`);
|
||||||
|
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));
|
||||||
|
|
||||||
132
src-tauri/Cargo.lock
generated
132
src-tauri/Cargo.lock
generated
@@ -34,8 +34,10 @@ dependencies = [
|
|||||||
"tauri-plugin-single-instance",
|
"tauri-plugin-single-instance",
|
||||||
"tauri-plugin-store",
|
"tauri-plugin-store",
|
||||||
"tauri-plugin-system-info",
|
"tauri-plugin-system-info",
|
||||||
|
"tauri-plugin-updater",
|
||||||
"tauri-plugin-valtio",
|
"tauri-plugin-valtio",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"url",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"window-vibrancy",
|
"window-vibrancy",
|
||||||
"winreg 0.55.0",
|
"winreg 0.55.0",
|
||||||
@@ -136,6 +138,15 @@ version = "1.0.100"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arbitrary"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||||
|
dependencies = [
|
||||||
|
"derive_arbitrary",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arboard"
|
name = "arboard"
|
||||||
version = "3.6.1"
|
version = "3.6.1"
|
||||||
@@ -981,6 +992,17 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_arbitrary"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.108",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_more"
|
name = "derive_more"
|
||||||
version = "0.99.20"
|
version = "0.99.20"
|
||||||
@@ -1327,6 +1349,18 @@ dependencies = [
|
|||||||
"rustc_version",
|
"rustc_version",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filetime"
|
||||||
|
version = "0.2.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"libredox",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
@@ -2552,6 +2586,7 @@ checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"libc",
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2684,6 +2719,12 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minisign-verify"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e856fdd13623a2f5f2f54676a4ee49502a96a80ef4a62bcedd23d52427c44d43"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
@@ -3139,6 +3180,18 @@ dependencies = [
|
|||||||
"objc2-foundation 0.2.2",
|
"objc2-foundation 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-osa-kit"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"objc2 0.6.3",
|
||||||
|
"objc2-app-kit",
|
||||||
|
"objc2-foundation 0.3.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-quartz-core"
|
name = "objc2-quartz-core"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -3318,6 +3371,20 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "osakit"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
|
||||||
|
dependencies = [
|
||||||
|
"objc2 0.6.3",
|
||||||
|
"objc2-foundation 0.3.2",
|
||||||
|
"objc2-osa-kit",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pango"
|
name = "pango"
|
||||||
version = "0.18.3"
|
version = "0.18.3"
|
||||||
@@ -4906,6 +4973,17 @@ dependencies = [
|
|||||||
"syn 2.0.108",
|
"syn 2.0.108",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tar"
|
||||||
|
version = "0.4.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
|
||||||
|
dependencies = [
|
||||||
|
"filetime",
|
||||||
|
"libc",
|
||||||
|
"xattr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "target-lexicon"
|
name = "target-lexicon"
|
||||||
version = "0.12.16"
|
version = "0.12.16"
|
||||||
@@ -5302,6 +5380,38 @@ dependencies = [
|
|||||||
"uom 0.35.0",
|
"uom 0.35.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-updater"
|
||||||
|
version = "2.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"dirs 6.0.0",
|
||||||
|
"flate2",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"infer",
|
||||||
|
"log",
|
||||||
|
"minisign-verify",
|
||||||
|
"osakit",
|
||||||
|
"percent-encoding",
|
||||||
|
"reqwest",
|
||||||
|
"semver",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tar",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"tempfile",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
"time",
|
||||||
|
"tokio",
|
||||||
|
"url",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
"zip",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-valtio"
|
name = "tauri-plugin-valtio"
|
||||||
version = "3.2.0"
|
version = "3.2.0"
|
||||||
@@ -7096,6 +7206,16 @@ version = "0.13.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
|
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xattr"
|
||||||
|
version = "1.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rustix 1.1.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xkeysym"
|
name = "xkeysym"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -7267,6 +7387,18 @@ dependencies = [
|
|||||||
"syn 2.0.108",
|
"syn 2.0.108",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zip"
|
||||||
|
version = "4.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
|
||||||
|
dependencies = [
|
||||||
|
"arbitrary",
|
||||||
|
"crc32fast",
|
||||||
|
"indexmap 2.12.0",
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zune-core"
|
name = "zune-core"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ notify = "8.2.0"
|
|||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
tokio = { version = "1.40", features = ["process"] }
|
tokio = { version = "1.40", features = ["process"] }
|
||||||
gfxinfo = "0.1.2"
|
gfxinfo = "0.1.2"
|
||||||
|
url = "2.5"
|
||||||
[target.'cfg(windows)'.dependencies] # Windows Only
|
[target.'cfg(windows)'.dependencies] # Windows Only
|
||||||
winreg = "0.55.0"
|
winreg = "0.55.0"
|
||||||
|
|
||||||
@@ -78,3 +79,4 @@ custom-protocol = [ "tauri/custom-protocol" ]
|
|||||||
tauri-plugin-cli = "2.4.1"
|
tauri-plugin-cli = "2.4.1"
|
||||||
tauri-plugin-global-shortcut = "2.3.1"
|
tauri-plugin-global-shortcut = "2.3.1"
|
||||||
tauri-plugin-single-instance = "2.3.6"
|
tauri-plugin-single-instance = "2.3.6"
|
||||||
|
tauri-plugin-updater = "2"
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
"fs:allow-read-text-file",
|
"fs:allow-read-text-file",
|
||||||
"fs:allow-write-text-file",
|
"fs:allow-write-text-file",
|
||||||
"fs:allow-resource-read-recursive",
|
"fs:allow-resource-read-recursive",
|
||||||
"dialog:allow-save"
|
"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","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"],"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"]}}
|
||||||
@@ -7262,6 +7262,60 @@
|
|||||||
"const": "system-info:deny-used-swap",
|
"const": "system-info:deny-used-swap",
|
||||||
"markdownDescription": "Denies the used_swap command without any pre-configured scope."
|
"markdownDescription": "Denies the used_swap command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:default",
|
||||||
|
"markdownDescription": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the check command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:allow-check",
|
||||||
|
"markdownDescription": "Enables the check command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the download command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:allow-download",
|
||||||
|
"markdownDescription": "Enables the download command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the download_and_install command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:allow-download-and-install",
|
||||||
|
"markdownDescription": "Enables the download_and_install command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the install command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:allow-install",
|
||||||
|
"markdownDescription": "Enables the install command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the check command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:deny-check",
|
||||||
|
"markdownDescription": "Denies the check command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the download command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:deny-download",
|
||||||
|
"markdownDescription": "Denies the download command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the download_and_install command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:deny-download-and-install",
|
||||||
|
"markdownDescription": "Denies the download_and_install command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the install command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:deny-install",
|
||||||
|
"markdownDescription": "Denies the install command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Default permissions for tauri-plugin-valtio.\n#### This default permission set includes:\n\n- `allow-allow-save`\n- `allow-allow-sync`\n- `allow-clear-autosave`\n- `allow-deny-save`\n- `allow-deny-sync`\n- `allow-destroy`\n- `allow-get-default-save-strategy`\n- `allow-get-save-strategy`\n- `allow-get-store-collection-path`\n- `allow-get-store-ids`\n- `allow-get-store-path`\n- `allow-get-store-state`\n- `allow-load`\n- `allow-patch`\n- `allow-save`\n- `allow-save-all`\n- `allow-save-all-now`\n- `allow-save-now`\n- `allow-save-some`\n- `allow-save-some-now`\n- `allow-set-autosave`\n- `allow-set-save-strategy`\n- `allow-set-store-collection-path`\n- `allow-set-store-options`\n- `allow-unload`",
|
"description": "Default permissions for tauri-plugin-valtio.\n#### This default permission set includes:\n\n- `allow-allow-save`\n- `allow-allow-sync`\n- `allow-clear-autosave`\n- `allow-deny-save`\n- `allow-deny-sync`\n- `allow-destroy`\n- `allow-get-default-save-strategy`\n- `allow-get-save-strategy`\n- `allow-get-store-collection-path`\n- `allow-get-store-ids`\n- `allow-get-store-path`\n- `allow-get-store-state`\n- `allow-load`\n- `allow-patch`\n- `allow-save`\n- `allow-save-all`\n- `allow-save-all-now`\n- `allow-save-now`\n- `allow-save-some`\n- `allow-save-some-now`\n- `allow-set-autosave`\n- `allow-set-save-strategy`\n- `allow-set-store-collection-path`\n- `allow-set-store-options`\n- `allow-unload`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -7262,6 +7262,60 @@
|
|||||||
"const": "system-info:deny-used-swap",
|
"const": "system-info:deny-used-swap",
|
||||||
"markdownDescription": "Denies the used_swap command without any pre-configured scope."
|
"markdownDescription": "Denies the used_swap command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:default",
|
||||||
|
"markdownDescription": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the check command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:allow-check",
|
||||||
|
"markdownDescription": "Enables the check command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the download command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:allow-download",
|
||||||
|
"markdownDescription": "Enables the download command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the download_and_install command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:allow-download-and-install",
|
||||||
|
"markdownDescription": "Enables the download_and_install command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the install command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:allow-install",
|
||||||
|
"markdownDescription": "Enables the install command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the check command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:deny-check",
|
||||||
|
"markdownDescription": "Denies the check command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the download command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:deny-download",
|
||||||
|
"markdownDescription": "Denies the download command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the download_and_install command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:deny-download-and-install",
|
||||||
|
"markdownDescription": "Denies the download_and_install command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the install command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:deny-install",
|
||||||
|
"markdownDescription": "Denies the install command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Default permissions for tauri-plugin-valtio.\n#### This default permission set includes:\n\n- `allow-allow-save`\n- `allow-allow-sync`\n- `allow-clear-autosave`\n- `allow-deny-save`\n- `allow-deny-sync`\n- `allow-destroy`\n- `allow-get-default-save-strategy`\n- `allow-get-save-strategy`\n- `allow-get-store-collection-path`\n- `allow-get-store-ids`\n- `allow-get-store-path`\n- `allow-get-store-state`\n- `allow-load`\n- `allow-patch`\n- `allow-save`\n- `allow-save-all`\n- `allow-save-all-now`\n- `allow-save-now`\n- `allow-save-some`\n- `allow-save-some-now`\n- `allow-set-autosave`\n- `allow-set-save-strategy`\n- `allow-set-store-collection-path`\n- `allow-set-store-options`\n- `allow-unload`",
|
"description": "Default permissions for tauri-plugin-valtio.\n#### This default permission set includes:\n\n- `allow-allow-save`\n- `allow-allow-sync`\n- `allow-clear-autosave`\n- `allow-deny-save`\n- `allow-deny-sync`\n- `allow-destroy`\n- `allow-get-default-save-strategy`\n- `allow-get-save-strategy`\n- `allow-get-store-collection-path`\n- `allow-get-store-ids`\n- `allow-get-store-path`\n- `allow-get-store-state`\n- `allow-load`\n- `allow-patch`\n- `allow-save`\n- `allow-save-all`\n- `allow-save-all-now`\n- `allow-save-now`\n- `allow-save-some`\n- `allow-save-some-now`\n- `allow-set-autosave`\n- `allow-set-save-strategy`\n- `allow-set-store-collection-path`\n- `allow-set-store-options`\n- `allow-unload`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::steam;
|
use crate::steam;
|
||||||
use crate::tool::updater::{check_update, download_update, install_update, UpdateInfo};
|
|
||||||
use crate::tool::*;
|
use crate::tool::*;
|
||||||
|
use tauri_plugin_updater::UpdaterExt;
|
||||||
use crate::vdf::preset;
|
use crate::vdf::preset;
|
||||||
use crate::vdf::preset::VideoConfig;
|
use crate::vdf::preset::VideoConfig;
|
||||||
use crate::wrap_err;
|
use crate::wrap_err;
|
||||||
@@ -8,12 +8,15 @@ use anyhow::Result;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{BufRead, BufReader};
|
use std::io::{BufRead, BufReader, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use reqwest;
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::sync::{Arc, OnceLock};
|
use std::sync::{Arc, OnceLock};
|
||||||
use tauri::path::BaseDirectory;
|
use tauri::path::BaseDirectory;
|
||||||
use tauri::Manager;
|
use tauri::{Emitter, Manager};
|
||||||
|
use url::Url;
|
||||||
|
use log::{debug, error, info, warn};
|
||||||
|
|
||||||
// use tauri_plugin_shell::ShellExt;
|
// use tauri_plugin_shell::ShellExt;
|
||||||
|
|
||||||
@@ -314,62 +317,345 @@ pub fn read_vprof_report(console_log_path: &str) -> Result<String, String> {
|
|||||||
|
|
||||||
// 更新相关命令
|
// 更新相关命令
|
||||||
|
|
||||||
/// 检查更新(支持 GitHub Release 和自定义端点)
|
/// 更新信息结构(用于前端)
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct UpdateInfo {
|
||||||
|
pub version: String,
|
||||||
|
pub notes: Option<String>,
|
||||||
|
pub download_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 包装结构,用于存储 update 对象和修改后的 CDN URL
|
||||||
|
struct UpdateWithCdnUrl {
|
||||||
|
update: tauri_plugin_updater::Update,
|
||||||
|
cdn_url: String,
|
||||||
|
file_path: Option<std::path::PathBuf>, // 保存下载的文件路径
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局存储待安装的更新
|
||||||
|
use std::sync::Mutex;
|
||||||
|
static PENDING_UPDATE: OnceLock<Mutex<Option<UpdateWithCdnUrl>>> = OnceLock::new();
|
||||||
|
|
||||||
|
fn get_pending_update() -> &'static Mutex<Option<UpdateWithCdnUrl>> {
|
||||||
|
PENDING_UPDATE.get_or_init(|| Mutex::new(None))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查更新(使用官方 updater 插件)
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn check_app_update(
|
pub async fn check_app_update(
|
||||||
app: tauri::AppHandle,
|
app: tauri::AppHandle,
|
||||||
endpoint: Option<String>,
|
endpoint: Option<String>,
|
||||||
use_mirror: Option<bool>,
|
|
||||||
include_prerelease: Option<bool>,
|
include_prerelease: Option<bool>,
|
||||||
) -> Result<Option<UpdateInfo>, String> {
|
) -> Result<Option<UpdateInfo>, String> {
|
||||||
let current_version = app.package_info().version.to_string();
|
info!("开始检查更新...");
|
||||||
let use_mirror = use_mirror.unwrap_or(false);
|
info!("include_prerelease: {:?}", include_prerelease);
|
||||||
let include_prerelease = include_prerelease.unwrap_or(false);
|
|
||||||
|
// 构建更新端点 URL
|
||||||
|
// Tauri updater 需要支持 {{target}} 和 {{arch}} 变量的端点
|
||||||
|
let update_url = if let Some(custom_endpoint) = endpoint {
|
||||||
|
info!("使用自定义端点: {}", custom_endpoint);
|
||||||
|
custom_endpoint
|
||||||
|
} else {
|
||||||
|
// 使用默认的 Tauri updater 格式端点
|
||||||
|
// 端点应该支持 {{target}} 和 {{arch}} 变量
|
||||||
|
const DEFAULT_GITHUB_REPO: &str = "plsgo/cstb";
|
||||||
|
let github_repo_str = std::env::var("GITHUB_REPO").ok();
|
||||||
|
let github_repo = github_repo_str.as_deref().unwrap_or(DEFAULT_GITHUB_REPO);
|
||||||
|
|
||||||
|
// 构建标准的 Tauri updater 端点格式
|
||||||
|
// 端点需要返回包含 platforms 字段的 JSON
|
||||||
|
let url = if include_prerelease.unwrap_or(false) {
|
||||||
|
format!("https://gh-info.okk.cool/repos/{}/releases/latest/pre/tauri", github_repo)
|
||||||
|
} else {
|
||||||
|
format!("https://gh-info.okk.cool/repos/{}/releases/latest/tauri", github_repo)
|
||||||
|
};
|
||||||
|
info!("使用默认端点: {}", url);
|
||||||
|
url
|
||||||
|
};
|
||||||
|
|
||||||
// 从环境变量获取 GitHub 仓库信息,如果没有则使用默认值
|
// 构建 updater
|
||||||
const DEFAULT_GITHUB_REPO: &str = "plsgo/cstb";
|
let mut builder = app.updater_builder();
|
||||||
let github_repo_str = std::env::var("GITHUB_REPO").ok();
|
|
||||||
let github_repo = github_repo_str.as_deref().unwrap_or(DEFAULT_GITHUB_REPO);
|
// 设置端点(如果提供了自定义端点,使用自定义端点;否则使用默认端点)
|
||||||
let result = wrap_err!(
|
let endpoint_url = Url::parse(&update_url)
|
||||||
check_update(
|
.map_err(|e| {
|
||||||
endpoint.as_deref(),
|
error!("解析更新端点 URL 失败: {} (URL: {})", e, update_url);
|
||||||
¤t_version,
|
format!("解析更新端点 URL 失败: {}", e)
|
||||||
use_mirror,
|
})?;
|
||||||
Some(github_repo),
|
info!("解析端点 URL 成功: {}", endpoint_url);
|
||||||
include_prerelease
|
|
||||||
)
|
builder = builder.endpoints(vec![endpoint_url])
|
||||||
.await
|
.map_err(|e| {
|
||||||
)?;
|
error!("设置更新端点失败: {}", e);
|
||||||
|
format!("设置更新端点失败: {}", e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 设置超时
|
||||||
|
builder = builder.timeout(std::time::Duration::from_secs(30));
|
||||||
|
info!("设置超时为 30 秒");
|
||||||
|
|
||||||
|
// 检查更新
|
||||||
|
let update = match builder.build() {
|
||||||
|
Ok(updater) => {
|
||||||
|
info!("构建 updater 成功,开始检查更新...");
|
||||||
|
updater.check().await
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("构建 updater 失败: {}", e);
|
||||||
|
return Err(format!("构建 updater 失败: {}", e));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
Ok(result)
|
match update {
|
||||||
|
Ok(Some(update)) => {
|
||||||
|
info!("发现新版本: {}", update.version);
|
||||||
|
info!("原始下载 URL: {}", update.download_url);
|
||||||
|
// Update 类型没有实现 Debug trait,所以不能使用 {:?} 格式化
|
||||||
|
// 如果需要更多信息,可以单独记录各个字段
|
||||||
|
|
||||||
|
// 将下载 URL 替换为 CDN 链接
|
||||||
|
let mut download_url = update.download_url.to_string();
|
||||||
|
let original_url = download_url.clone();
|
||||||
|
// 如果 URL 不是 CDN 链接,则在 CDN 域名后拼接原 URL
|
||||||
|
if !download_url.contains("cdn.upup.cool") {
|
||||||
|
download_url = format!("https://cdn.upup.cool/{}", original_url);
|
||||||
|
info!("将下载 URL 从 {} 替换为 CDN 链接: {}", original_url, download_url);
|
||||||
|
} else {
|
||||||
|
info!("下载 URL 已经是 CDN 链接: {}", download_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储更新对象和 CDN URL 供后续使用
|
||||||
|
let pending = get_pending_update();
|
||||||
|
*pending.lock().unwrap() = Some(UpdateWithCdnUrl {
|
||||||
|
update: update.clone(),
|
||||||
|
cdn_url: download_url.clone(),
|
||||||
|
file_path: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 转换为前端需要的格式
|
||||||
|
let update_info = UpdateInfo {
|
||||||
|
version: update.version.to_string(),
|
||||||
|
notes: update.body.clone(),
|
||||||
|
download_url: download_url.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("更新信息准备完成 - 版本: {}, 下载 URL: {}", update_info.version, download_url);
|
||||||
|
Ok(Some(update_info))
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
info!("当前已是最新版本");
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("检查更新失败: {}", e);
|
||||||
|
Err(format!("检查更新失败: {}", e))
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 下载更新
|
/// 下载更新
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn download_app_update(
|
pub async fn download_app_update(
|
||||||
app: tauri::AppHandle,
|
app: tauri::AppHandle,
|
||||||
download_url: String,
|
) -> Result<(), String> {
|
||||||
) -> Result<String, String> {
|
info!("开始下载更新...");
|
||||||
// 重置取消标志
|
let pending = get_pending_update();
|
||||||
let cancelled = get_download_cancelled();
|
|
||||||
cancelled.store(false, Ordering::Relaxed);
|
// 检查是否有待下载的更新
|
||||||
|
let has_update = {
|
||||||
let path = wrap_err!(download_update(&app, &download_url, cancelled).await)?;
|
let update_guard = pending.lock().unwrap();
|
||||||
Ok(path.to_string_lossy().to_string())
|
update_guard.is_some()
|
||||||
|
};
|
||||||
|
|
||||||
|
if !has_update {
|
||||||
|
warn!("没有待下载的更新");
|
||||||
|
return Err("没有待下载的更新".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听下载进度
|
||||||
|
// 克隆 app_handle 用于两个闭包
|
||||||
|
let app_handle_progress = app.clone();
|
||||||
|
let app_handle_complete = app.clone();
|
||||||
|
|
||||||
|
// 在锁内获取 update 和 CDN URL,然后在锁外使用
|
||||||
|
let cloned_data = {
|
||||||
|
let update_guard = pending.lock().unwrap();
|
||||||
|
if let Some(ref update_with_cdn) = *update_guard {
|
||||||
|
info!("准备下载更新 - 版本: {}, 原始 URL: {}, CDN URL: {}",
|
||||||
|
update_with_cdn.update.version,
|
||||||
|
update_with_cdn.update.download_url,
|
||||||
|
update_with_cdn.cdn_url);
|
||||||
|
Some((update_with_cdn.update.clone(), update_with_cdn.cdn_url.clone()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 现在锁已经释放,可以安全地下载
|
||||||
|
if let Some((update, cdn_url)) = cloned_data {
|
||||||
|
info!("开始使用 CDN URL 下载更新文件: {}", cdn_url);
|
||||||
|
|
||||||
|
// 使用 reqwest 手动下载文件
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let mut response = client
|
||||||
|
.get(&cdn_url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("下载更新失败: {}", e);
|
||||||
|
format!("下载更新失败: {}", e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 获取文件大小(用于进度计算)
|
||||||
|
let content_length = response.content_length();
|
||||||
|
let mut downloaded: u64 = 0;
|
||||||
|
|
||||||
|
// 创建临时文件
|
||||||
|
let temp_dir = std::env::temp_dir();
|
||||||
|
let file_name = Path::new(update.download_url.as_str())
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("update.exe");
|
||||||
|
let temp_file_path = temp_dir.join(file_name);
|
||||||
|
|
||||||
|
info!("临时文件路径: {:?}", temp_file_path);
|
||||||
|
|
||||||
|
// 创建文件并写入
|
||||||
|
let mut file = std::fs::File::create(&temp_file_path)
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("创建临时文件失败: {}", e);
|
||||||
|
format!("创建临时文件失败: {}", e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 下载并写入文件,同时报告进度
|
||||||
|
while let Some(chunk) = response.chunk().await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("下载更新失败: {}", e);
|
||||||
|
format!("下载更新失败: {}", e)
|
||||||
|
})? {
|
||||||
|
file.write_all(&chunk)
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("写入文件失败: {}", e);
|
||||||
|
format!("写入文件失败: {}", e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
downloaded += chunk.len() as u64;
|
||||||
|
|
||||||
|
// 计算并报告进度
|
||||||
|
if let Some(total) = content_length {
|
||||||
|
let progress = (downloaded * 100) / total;
|
||||||
|
debug!("下载进度: {} / {} ({}%)", downloaded, total, progress);
|
||||||
|
let _ = app_handle_progress.emit("update-download-progress", progress);
|
||||||
|
} else {
|
||||||
|
debug!("下载进度: {} 字节", downloaded);
|
||||||
|
let _ = app_handle_progress.emit("update-download-progress", downloaded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("文件下载完成,大小: {} 字节", downloaded);
|
||||||
|
|
||||||
|
// 下载完成
|
||||||
|
let _ = app_handle_complete.emit("update-download-progress", 100u64);
|
||||||
|
|
||||||
|
// 注意:由于我们手动下载了文件,我们需要确保 update 对象知道文件的位置
|
||||||
|
// 但是,update.download() 方法可能还会验证签名等,所以我们需要确保手动下载的文件也能通过验证
|
||||||
|
// 目前,我们仍然使用原始的 update 对象,但文件已经下载到临时目录
|
||||||
|
// 如果 update.install() 需要文件路径,我们可能需要修改它
|
||||||
|
|
||||||
|
// 更新存储的 update 对象,保存文件路径
|
||||||
|
let mut update_guard = pending.lock().unwrap();
|
||||||
|
*update_guard = Some(UpdateWithCdnUrl {
|
||||||
|
update,
|
||||||
|
cdn_url,
|
||||||
|
file_path: Some(temp_file_path.clone()),
|
||||||
|
});
|
||||||
|
info!("更新文件下载完成并已存储,文件路径: {:?}", temp_file_path);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
warn!("没有待下载的更新(克隆失败)");
|
||||||
|
Err("没有待下载的更新".to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 取消下载
|
/// 取消下载
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn cancel_download_update() -> Result<(), String> {
|
pub fn cancel_download_update() -> Result<(), String> {
|
||||||
let cancelled = get_download_cancelled();
|
info!("取消下载更新");
|
||||||
cancelled.store(true, Ordering::Relaxed);
|
// 官方 updater 插件没有直接的取消方法
|
||||||
|
// 可以通过删除待安装的更新来实现
|
||||||
|
let pending = get_pending_update();
|
||||||
|
*pending.lock().unwrap() = None;
|
||||||
|
info!("已清除待下载的更新");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 安装更新
|
/// 安装更新
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn install_app_update(installer_path: String) -> Result<(), String> {
|
pub fn install_app_update(_app: tauri::AppHandle) -> Result<(), String> {
|
||||||
wrap_err!(install_update(&installer_path))
|
info!("开始安装更新...");
|
||||||
|
let pending = get_pending_update();
|
||||||
|
let mut update_guard = pending.lock().unwrap();
|
||||||
|
|
||||||
|
if let Some(update_with_cdn) = update_guard.take() {
|
||||||
|
let update = update_with_cdn.update;
|
||||||
|
info!("准备安装更新 - 版本: {}", update.version);
|
||||||
|
info!("下载 URL: {}", update.download_url);
|
||||||
|
|
||||||
|
// 使用 tauri updater 的 install API,传递已下载文件的字节内容
|
||||||
|
// 这样可以确保 tauri updater 正确处理应用的关闭和重启逻辑
|
||||||
|
if let Some(ref file_path) = update_with_cdn.file_path {
|
||||||
|
if file_path.exists() {
|
||||||
|
info!("找到下载的安装程序: {:?}", file_path);
|
||||||
|
|
||||||
|
// 读取文件内容
|
||||||
|
let file_bytes = std::fs::read(file_path)
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("读取安装文件失败: {}", e);
|
||||||
|
format!("读取安装文件失败: {}", e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
info!("读取安装文件成功,大小: {} 字节", file_bytes.len());
|
||||||
|
|
||||||
|
// 使用 tauri updater 的 install 方法,传递文件字节内容
|
||||||
|
// 这样 tauri updater 可以正确处理应用的关闭和重启
|
||||||
|
match update.install(&file_bytes) {
|
||||||
|
Ok(_) => {
|
||||||
|
info!("安装更新成功,应用将退出以完成安装");
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("安装更新失败: {}", e);
|
||||||
|
error!("错误详情: {:?}", e);
|
||||||
|
let error_msg = format!("安装更新失败: {}", e);
|
||||||
|
Err(error_msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("下载的安装程序不存在: {:?}", file_path);
|
||||||
|
Err(format!("下载的安装程序不存在: {:?}", file_path))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("没有找到下载的文件路径,尝试使用 update.install() 空参数");
|
||||||
|
// 如果没有文件路径,尝试使用 update.install() 空参数
|
||||||
|
// 这可能会让 updater 自己下载文件
|
||||||
|
match update.install(&[]) {
|
||||||
|
Ok(_) => {
|
||||||
|
info!("安装更新成功,应用将退出以完成安装");
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("安装更新失败: {}", e);
|
||||||
|
error!("错误详情: {:?}", e);
|
||||||
|
let error_msg = format!("安装更新失败: {}", e);
|
||||||
|
Err(error_msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("没有待安装的更新");
|
||||||
|
Err("没有待安装的更新".to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取 PowerShell Get-ComputerInfo 信息(异步版本)
|
/// 获取 PowerShell Get-ComputerInfo 信息(异步版本)
|
||||||
@@ -378,8 +664,6 @@ pub fn install_app_update(installer_path: String) -> Result<(), String> {
|
|||||||
pub async fn get_computer_info() -> Result<serde_json::Value, String> {
|
pub async fn get_computer_info() -> Result<serde_json::Value, String> {
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use std::os::windows::process::CommandExt;
|
|
||||||
#[cfg(windows)]
|
|
||||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||||
|
|
||||||
// 异步执行 PowerShell 命令获取计算机信息并转换为 JSON
|
// 异步执行 PowerShell 命令获取计算机信息并转换为 JSON
|
||||||
@@ -565,8 +849,6 @@ pub struct MotherboardInfo {
|
|||||||
pub async fn get_memory_info() -> Result<Vec<MemoryInfo>, String> {
|
pub async fn get_memory_info() -> Result<Vec<MemoryInfo>, String> {
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use std::os::windows::process::CommandExt;
|
|
||||||
#[cfg(windows)]
|
|
||||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||||
|
|
||||||
// 执行 PowerShell 命令获取内存信息
|
// 执行 PowerShell 命令获取内存信息
|
||||||
@@ -657,8 +939,6 @@ pub async fn get_memory_info() -> Result<Vec<MemoryInfo>, String> {
|
|||||||
pub async fn get_monitor_info() -> Result<Vec<MonitorInfo>, String> {
|
pub async fn get_monitor_info() -> Result<Vec<MonitorInfo>, String> {
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use std::os::windows::process::CommandExt;
|
|
||||||
#[cfg(windows)]
|
|
||||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||||
|
|
||||||
// 执行 PowerShell 命令获取显示器信息
|
// 执行 PowerShell 命令获取显示器信息
|
||||||
@@ -823,8 +1103,6 @@ pub async fn get_monitor_info() -> Result<Vec<MonitorInfo>, String> {
|
|||||||
pub async fn get_motherboard_info() -> Result<MotherboardInfo, String> {
|
pub async fn get_motherboard_info() -> Result<MotherboardInfo, String> {
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use std::os::windows::process::CommandExt;
|
|
||||||
#[cfg(windows)]
|
|
||||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||||
|
|
||||||
// 执行 PowerShell 命令获取主板信息
|
// 执行 PowerShell 命令获取主板信息
|
||||||
|
|||||||
@@ -3,19 +3,19 @@
|
|||||||
windows_subsystem = "windows"
|
windows_subsystem = "windows"
|
||||||
)]
|
)]
|
||||||
|
|
||||||
use std::thread;
|
|
||||||
use std::time::Duration;
|
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
use tauri_plugin_autostart::MacosLauncher;
|
|
||||||
use tauri_plugin_cli::CliExt;
|
|
||||||
use tauri_plugin_deep_link::DeepLinkExt;
|
|
||||||
use tauri_plugin_store::StoreExt;
|
use tauri_plugin_store::StoreExt;
|
||||||
|
use tauri_plugin_deep_link::DeepLinkExt;
|
||||||
|
use tauri_plugin_cli::CliExt;
|
||||||
|
use tauri_plugin_autostart::MacosLauncher;
|
||||||
|
|
||||||
// Window Vibrancy
|
// Window Vibrancy
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
use window_vibrancy::apply_acrylic;
|
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};
|
||||||
|
|
||||||
@@ -48,27 +48,13 @@ fn main() {
|
|||||||
let app_name = ctx.config().identifier.as_str();
|
let app_name = ctx.config().identifier.as_str();
|
||||||
let store_dir = config_dir.join(app_name).join("cstb");
|
let store_dir = config_dir.join(app_name).join("cstb");
|
||||||
|
|
||||||
tauri::Builder::default()
|
let mut builder = tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_single_instance::init(
|
.plugin(tauri_plugin_single_instance::init(|app, _, _| {
|
||||||
|app: &tauri::AppHandle, args: Vec<String>, _cwd: String| {
|
let window = app.get_webview_window("main").expect("no main window");
|
||||||
// 检查是否是"更新启动"的特殊请求
|
|
||||||
let is_update_launch = args.contains(&"--update-launch".to_string());
|
|
||||||
|
|
||||||
if is_update_launch {
|
window.show().expect("no main window, can't show");
|
||||||
// 若存在旧实例,强制关闭(避免残留进程阻止新实例)
|
window.set_focus().expect("no main window, can't set focus")
|
||||||
if let Some(old_window) = app.get_webview_window("main") {
|
}))
|
||||||
// 先尝试优雅关闭,再强制退出
|
|
||||||
old_window.close().ok();
|
|
||||||
app.exit(0);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 常规单实例逻辑:激活现有窗口
|
|
||||||
let window = app.get_webview_window("main").expect("no main window");
|
|
||||||
window.show().expect("can't show main window");
|
|
||||||
window.set_focus().expect("can't focus main window");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
))
|
|
||||||
.plugin(tauri_plugin_deep_link::init())
|
.plugin(tauri_plugin_deep_link::init())
|
||||||
.plugin(tauri_plugin_store::Builder::new().build())
|
.plugin(tauri_plugin_store::Builder::new().build())
|
||||||
.plugin(tauri_plugin_notification::init())
|
.plugin(tauri_plugin_notification::init())
|
||||||
@@ -87,9 +73,14 @@ fn main() {
|
|||||||
.plugin(tauri_plugin_system_info::init())
|
.plugin(tauri_plugin_system_info::init())
|
||||||
.plugin(tauri_plugin_cli::init())
|
.plugin(tauri_plugin_cli::init())
|
||||||
// .plugin(tauri_plugin_valtio::init())
|
// .plugin(tauri_plugin_valtio::init())
|
||||||
.plugin(tauri_plugin_valtio::Builder::new().path(&store_dir).build())
|
.plugin(tauri_plugin_valtio::Builder::new().path(&store_dir).build());
|
||||||
// .plugin(tauri_plugin_updater::Builder::new().build())
|
|
||||||
.setup(move |app| {
|
#[cfg(desktop)]
|
||||||
|
{
|
||||||
|
builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.setup(move |app| {
|
||||||
// Get Window
|
// Get Window
|
||||||
let window = app.get_webview_window("main").unwrap();
|
let window = app.get_webview_window("main").unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
pub mod common;
|
pub mod common;
|
||||||
pub mod macros;
|
pub mod macros;
|
||||||
pub mod powerplan;
|
pub mod powerplan;
|
||||||
pub mod updater;
|
// pub mod updater; // 已迁移到官方 tauri-plugin-updater
|
||||||
|
|||||||
@@ -1,457 +0,0 @@
|
|||||||
use anyhow::{Context, Result};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::fs;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::process::Command;
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tauri::path::BaseDirectory;
|
|
||||||
use tauri::{Emitter, Manager};
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
use std::os::windows::process::CommandExt;
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
|
||||||
|
|
||||||
/// 更新信息结构
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct UpdateInfo {
|
|
||||||
pub version: String,
|
|
||||||
pub notes: Option<String>,
|
|
||||||
pub download_url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// gh-info API 响应结构
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
struct GhInfoApiResponse {
|
|
||||||
repo: String,
|
|
||||||
latest_version: String,
|
|
||||||
changelog: Option<String>,
|
|
||||||
published_at: String,
|
|
||||||
#[serde(default)]
|
|
||||||
prerelease: bool,
|
|
||||||
attachments: serde_json::Value, // 支持两种格式: ["URL1", "URL2"] 或 [["文件名", "URL"], ...]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 自定义更新服务器 API 响应结构
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
struct CustomUpdateApiResponse {
|
|
||||||
version: String,
|
|
||||||
notes: Option<String>,
|
|
||||||
#[serde(rename = "pub_date")]
|
|
||||||
pub_date: Option<String>,
|
|
||||||
download_url: String,
|
|
||||||
signature: Option<String>,
|
|
||||||
platforms: Option<std::collections::HashMap<String, PlatformInfo>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 平台特定信息
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
struct PlatformInfo {
|
|
||||||
url: String,
|
|
||||||
signature: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 检查更新(使用自定义 API 端点)
|
|
||||||
pub async fn check_update(
|
|
||||||
endpoint: Option<&str>,
|
|
||||||
current_version: &str,
|
|
||||||
_use_mirror: bool,
|
|
||||||
github_repo: Option<&str>,
|
|
||||||
include_prerelease: bool,
|
|
||||||
) -> Result<Option<UpdateInfo>> {
|
|
||||||
// 确定使用的 API 端点
|
|
||||||
let api_url = if let Some(custom_endpoint) = endpoint {
|
|
||||||
// 如果提供了自定义端点,直接使用
|
|
||||||
custom_endpoint.to_string()
|
|
||||||
} else {
|
|
||||||
// 否则使用默认的 gh-info API
|
|
||||||
let repo = github_repo.unwrap_or("plsgo/cstb");
|
|
||||||
if include_prerelease {
|
|
||||||
format!(
|
|
||||||
"https://gh-info.okk.cool/repos/{}/releases/latest/pre",
|
|
||||||
repo
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
format!("https://gh-info.okk.cool/repos/{}/releases/latest", repo)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
|
||||||
.timeout(std::time::Duration::from_secs(10))
|
|
||||||
.user_agent("cstb-updater/1.0")
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let response = client.get(&api_url).send().await?;
|
|
||||||
|
|
||||||
if !response.status().is_success() {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"API 请求失败,HTTP 状态码: {}",
|
|
||||||
response.status()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取响应文本以便尝试不同的解析方式
|
|
||||||
let response_text = response.text().await?;
|
|
||||||
// 关闭更新日志的打印
|
|
||||||
// println!("[更新检查] API 响应: {}", response_text);
|
|
||||||
|
|
||||||
// 尝试解析为自定义更新服务器格式
|
|
||||||
let update_info = if let Ok(custom_resp) =
|
|
||||||
serde_json::from_str::<CustomUpdateApiResponse>(&response_text)
|
|
||||||
{
|
|
||||||
// 提取版本号(去掉 'v' 前缀)
|
|
||||||
let version = custom_resp.version.trim_start_matches('v').to_string();
|
|
||||||
|
|
||||||
// 版本比较
|
|
||||||
let comparison = compare_version(&version, current_version);
|
|
||||||
if comparison > 0 {
|
|
||||||
// 获取下载链接
|
|
||||||
// 优先使用平台特定的链接
|
|
||||||
let download_url = if let Some(ref platforms) = custom_resp.platforms {
|
|
||||||
// 检测当前平台
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
let platform_key = "windows-x86_64";
|
|
||||||
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
|
|
||||||
let platform_key = "darwin-x86_64";
|
|
||||||
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
|
|
||||||
let platform_key = "darwin-aarch64";
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
let platform_key = "linux-x86_64";
|
|
||||||
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
|
||||||
let platform_key = "";
|
|
||||||
|
|
||||||
if !platform_key.is_empty() {
|
|
||||||
platforms
|
|
||||||
.get(platform_key)
|
|
||||||
.map(|p| p.url.clone())
|
|
||||||
.unwrap_or_else(|| custom_resp.download_url.clone())
|
|
||||||
} else {
|
|
||||||
custom_resp.download_url.clone()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
custom_resp.download_url.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(UpdateInfo {
|
|
||||||
version,
|
|
||||||
notes: custom_resp.notes,
|
|
||||||
download_url,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 尝试解析为 gh-info API 格式
|
|
||||||
let api_resp: GhInfoApiResponse = serde_json::from_str(&response_text)
|
|
||||||
.context("解析更新 API 响应失败,既不是自定义格式也不是 gh-info 格式")?;
|
|
||||||
|
|
||||||
// 提取版本号(去掉 'v' 前缀)
|
|
||||||
let version = api_resp.latest_version.trim_start_matches('v').to_string();
|
|
||||||
|
|
||||||
// 版本比较
|
|
||||||
let comparison = compare_version(&version, current_version);
|
|
||||||
|
|
||||||
if comparison > 0 {
|
|
||||||
// 从 attachments 中获取下载链接
|
|
||||||
// 支持两种格式:
|
|
||||||
// 1. 字符串数组: ["URL1", "URL2", ...]
|
|
||||||
// 2. 嵌套数组: [["文件名", "URL"], ...]
|
|
||||||
let download_url = extract_download_url(&api_resp.attachments)
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("未找到可下载的安装包"))?;
|
|
||||||
|
|
||||||
Some(UpdateInfo {
|
|
||||||
version,
|
|
||||||
notes: api_resp.changelog,
|
|
||||||
download_url,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(update_info)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 从 attachments 中提取下载 URL
|
|
||||||
/// 支持两种格式:
|
|
||||||
/// 1. 字符串数组: ["URL1", "URL2", ...] - 优先选择 .exe 或 .msi 文件
|
|
||||||
/// 2. 嵌套数组: [["文件名", "URL"], ...] - 优先选择 .exe 或 .msi 文件
|
|
||||||
fn extract_download_url(attachments: &serde_json::Value) -> Option<String> {
|
|
||||||
// 尝试解析为字符串数组格式: ["URL1", "URL2", ...]
|
|
||||||
if let Ok(urls) = serde_json::from_value::<Vec<String>>(attachments.clone()) {
|
|
||||||
// 优先选择 .exe 或 .msi 文件
|
|
||||||
if let Some(url) = urls
|
|
||||||
.iter()
|
|
||||||
.find(|url| url.ends_with(".exe") || url.ends_with(".msi"))
|
|
||||||
{
|
|
||||||
return Some(url.clone());
|
|
||||||
}
|
|
||||||
// 如果没有找到 .exe 或 .msi,使用第一个 URL
|
|
||||||
return urls.first().cloned();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试解析为嵌套数组格式: [["文件名", "URL"], ...]
|
|
||||||
if let Ok(nested) = serde_json::from_value::<Vec<Vec<String>>>(attachments.clone()) {
|
|
||||||
// 优先选择 .exe 或 .msi 文件
|
|
||||||
if let Some(url) = nested.iter().find_map(|attachment| {
|
|
||||||
if attachment.len() >= 2 {
|
|
||||||
let filename = &attachment[0];
|
|
||||||
let url = &attachment[1];
|
|
||||||
if filename.ends_with(".exe") || filename.ends_with(".msi") {
|
|
||||||
Some(url.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
return Some(url);
|
|
||||||
}
|
|
||||||
// 如果没有找到 .exe 或 .msi,使用第一个附件的 URL
|
|
||||||
if let Some(attachment) = nested.first() {
|
|
||||||
if attachment.len() >= 2 {
|
|
||||||
return Some(attachment[1].clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 改进的版本比较函数,支持预发布版本(beta.5, beta.6等)
|
|
||||||
fn compare_version(new: &str, current: &str) -> i32 {
|
|
||||||
// 解析版本号:支持格式如 "0.0.6-beta.5", "beta.6", "0.0.6" 等
|
|
||||||
let (new_base, new_pre) = parse_version(new);
|
|
||||||
let (current_base, current_pre) = parse_version(current);
|
|
||||||
|
|
||||||
// 先比较基础版本号(数字部分)
|
|
||||||
let base_comparison = compare_version_parts(&new_base, ¤t_base);
|
|
||||||
|
|
||||||
if base_comparison != 0 {
|
|
||||||
return base_comparison;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果基础版本相同(或都为空),比较预发布标识符
|
|
||||||
// 如果基础版本都为空,说明是纯预发布版本(如 beta.5 vs beta.6)
|
|
||||||
let pre_comparison = compare_prerelease(&new_pre, ¤t_pre);
|
|
||||||
|
|
||||||
// 如果基础版本都为空且预发布比较结果为0,说明版本完全相同
|
|
||||||
if new_base.is_empty() && current_base.is_empty() && pre_comparison == 0 {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre_comparison
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 解析版本号,返回(基础版本号数组,预发布标识符)
|
|
||||||
fn parse_version(version: &str) -> (Vec<u32>, Option<String>) {
|
|
||||||
// 去掉 'v' 前缀
|
|
||||||
let version = version.trim_start_matches('v').trim();
|
|
||||||
|
|
||||||
// 检查是否有预发布标识符(如 -beta.5, -alpha.1 等)
|
|
||||||
let (base_str, pre_str) = if let Some(dash_pos) = version.find('-') {
|
|
||||||
let (base, pre) = version.split_at(dash_pos);
|
|
||||||
(base, Some(pre[1..].to_string())) // 跳过 '-' 字符
|
|
||||||
} else {
|
|
||||||
(version, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
// 解析基础版本号(数字部分)
|
|
||||||
let base_parts: Vec<u32> = base_str.split('.').filter_map(|s| s.parse().ok()).collect();
|
|
||||||
|
|
||||||
// 如果基础版本号为空且没有预发布标识符,可能是纯预发布版本(如 "beta.5")
|
|
||||||
// 这种情况下,整个字符串作为预发布标识符
|
|
||||||
if base_parts.is_empty() && pre_str.is_none() {
|
|
||||||
// 检查是否包含非数字字符(可能是预发布版本)
|
|
||||||
if !version.chars().any(|c| c.is_ascii_digit()) {
|
|
||||||
return (vec![], Some(version.to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(base_parts, pre_str)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 比较版本号数组(数字部分)
|
|
||||||
fn compare_version_parts(new: &[u32], current: &[u32]) -> i32 {
|
|
||||||
let max_len = new.len().max(current.len());
|
|
||||||
|
|
||||||
for i in 0..max_len {
|
|
||||||
let new_val = new.get(i).copied().unwrap_or(0);
|
|
||||||
let current_val = current.get(i).copied().unwrap_or(0);
|
|
||||||
|
|
||||||
if new_val > current_val {
|
|
||||||
return 1;
|
|
||||||
} else if new_val < current_val {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 比较预发布标识符
|
|
||||||
/// 规则:
|
|
||||||
/// - 有预发布标识符的版本 < 没有预发布标识符的版本
|
|
||||||
/// - 如果都有预发布标识符,按字典序比较
|
|
||||||
fn compare_prerelease(new: &Option<String>, current: &Option<String>) -> i32 {
|
|
||||||
match (new, current) {
|
|
||||||
// 都没有预发布标识符,版本相同
|
|
||||||
(None, None) => 0,
|
|
||||||
// 新版本有预发布,当前版本没有 -> 新版本更旧(预发布版本 < 正式版本)
|
|
||||||
(Some(_), None) => -1,
|
|
||||||
// 新版本没有预发布,当前版本有 -> 新版本更新
|
|
||||||
(None, Some(_)) => 1,
|
|
||||||
// 都有预发布标识符,按字典序比较
|
|
||||||
(Some(new_pre), Some(current_pre)) => {
|
|
||||||
// 尝试提取数字部分进行比较(如 beta.5 -> 5, beta.6 -> 6)
|
|
||||||
let new_num = extract_number_from_prerelease(new_pre);
|
|
||||||
let current_num = extract_number_from_prerelease(current_pre);
|
|
||||||
|
|
||||||
if let (Some(new_n), Some(current_n)) = (new_num, current_num) {
|
|
||||||
// 如果都能提取数字,比较数字
|
|
||||||
if new_n > current_n {
|
|
||||||
1
|
|
||||||
} else if new_n < current_n {
|
|
||||||
-1
|
|
||||||
} else {
|
|
||||||
// 数字相同,按字符串比较
|
|
||||||
new_pre.cmp(current_pre) as i32
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 无法提取数字,按字符串比较
|
|
||||||
new_pre.cmp(current_pre) as i32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 从预发布标识符中提取数字(如 "beta.5" -> 5, "alpha.1" -> 1)
|
|
||||||
fn extract_number_from_prerelease(pre: &str) -> Option<u32> {
|
|
||||||
// 尝试从最后一部分提取数字
|
|
||||||
if let Some(last_part) = pre.split('.').last() {
|
|
||||||
last_part.parse().ok()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 下载更新文件(带进度追踪和取消支持)
|
|
||||||
pub async fn download_update(
|
|
||||||
app: &tauri::AppHandle,
|
|
||||||
download_url: &str,
|
|
||||||
cancelled: Arc<AtomicBool>,
|
|
||||||
) -> Result<PathBuf> {
|
|
||||||
let client = reqwest::Client::builder()
|
|
||||||
.timeout(std::time::Duration::from_secs(300))
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let response = client.get(download_url).send().await?;
|
|
||||||
|
|
||||||
if !response.status().is_success() {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"下载失败,HTTP 状态码: {}",
|
|
||||||
response.status()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取文件总大小
|
|
||||||
let total_size = response.content_length().unwrap_or(0);
|
|
||||||
|
|
||||||
// 获取缓存目录
|
|
||||||
let cache_dir = app
|
|
||||||
.path()
|
|
||||||
.resolve("updates", BaseDirectory::AppCache)
|
|
||||||
.context("无法获取缓存目录")?;
|
|
||||||
|
|
||||||
fs::create_dir_all(&cache_dir)?;
|
|
||||||
|
|
||||||
// 从 URL 中提取文件名
|
|
||||||
let filename = download_url
|
|
||||||
.split('/')
|
|
||||||
.last()
|
|
||||||
.unwrap_or("update")
|
|
||||||
.split('?')
|
|
||||||
.next()
|
|
||||||
.unwrap_or("update");
|
|
||||||
|
|
||||||
let file_path = cache_dir.join(filename);
|
|
||||||
|
|
||||||
// 下载文件
|
|
||||||
let mut file = fs::File::create(&file_path)?;
|
|
||||||
let mut stream = response.bytes_stream();
|
|
||||||
let mut downloaded: u64 = 0;
|
|
||||||
|
|
||||||
use futures_util::StreamExt;
|
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
while let Some(item) = stream.next().await {
|
|
||||||
// 检查是否取消
|
|
||||||
if cancelled.load(Ordering::Relaxed) {
|
|
||||||
// 删除部分下载的文件
|
|
||||||
let _ = fs::remove_file(&file_path);
|
|
||||||
return Err(anyhow::anyhow!("下载已取消"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let chunk = item?;
|
|
||||||
file.write_all(&chunk)?;
|
|
||||||
downloaded += chunk.len() as u64;
|
|
||||||
|
|
||||||
// 发送进度事件
|
|
||||||
if total_size > 0 {
|
|
||||||
let progress = (downloaded * 100) / total_size;
|
|
||||||
let _ = app.emit("update-download-progress", progress);
|
|
||||||
} else {
|
|
||||||
// 如果无法获取总大小,发送已下载的字节数
|
|
||||||
let _ = app.emit("update-download-progress", downloaded);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
file.sync_all()?;
|
|
||||||
|
|
||||||
// 发送完成事件
|
|
||||||
let _ = app.emit("update-download-progress", 100u64);
|
|
||||||
Ok(file_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 安装更新(Windows)
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub fn install_update(installer_path: &str) -> Result<()> {
|
|
||||||
// 使用 /S 静默安装
|
|
||||||
let mut cmd = Command::new(installer_path);
|
|
||||||
cmd.args(&["/S", "/appParam=\"--update-launch\""]); // 静默安装
|
|
||||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
|
||||||
let mut child = cmd.spawn()?;
|
|
||||||
// 等待安装程序完成
|
|
||||||
child.wait()?;
|
|
||||||
// 安装完成后,由前端调用 relaunch() 来启动新版本
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 安装更新(macOS)
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
pub fn install_update(installer_path: &str) -> Result<()> {
|
|
||||||
let mut child = Command::new("open").arg(installer_path).spawn()?;
|
|
||||||
// 等待安装程序完成
|
|
||||||
child.wait()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 安装更新(Linux)
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub fn install_update(installer_path: &str) -> Result<()> {
|
|
||||||
if installer_path.ends_with(".deb") {
|
|
||||||
let mut child = Command::new("sudo")
|
|
||||||
.args(&["dpkg", "-i", installer_path])
|
|
||||||
.spawn()?;
|
|
||||||
child.wait()?;
|
|
||||||
} else if installer_path.ends_with(".AppImage") {
|
|
||||||
let mut child = Command::new("chmod")
|
|
||||||
.args(&["+x", installer_path])
|
|
||||||
.spawn()?;
|
|
||||||
child.wait()?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
"resources": [
|
"resources": [
|
||||||
"resources/csda.exe"
|
"resources/csda.exe"
|
||||||
],
|
],
|
||||||
|
"createUpdaterArtifacts": true,
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
@@ -69,6 +70,14 @@
|
|||||||
"description": "hidden on start"
|
"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": {
|
||||||
|
|||||||
@@ -24,78 +24,77 @@ export default function Page() {
|
|||||||
return (
|
return (
|
||||||
<section className="flex flex-col gap-4 overflow-hidden">
|
<section className="flex flex-col gap-4 overflow-hidden">
|
||||||
<div className="flex flex-col items-start gap-4 pt-2 pb-1">
|
<div className="flex flex-col items-start gap-4 pt-2 pb-1">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<p className="text-sm">版本号:{app.state.version}</p>
|
<p className="text-sm">版本号:{app.state.version}</p>
|
||||||
{app.state.hasUpdate && app.state.latestVersion && (
|
{app.state.hasUpdate && app.state.latestVersion && (
|
||||||
<Chip size="sm" color="success" variant="flat">
|
<Chip size="sm" color="success" variant="flat">
|
||||||
{app.state.latestVersion}
|
{app.state.latestVersion}
|
||||||
</Chip>
|
</Chip>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
{/* <p className="text-sm">是否有更新:{app.state.hasUpdate ? "有" : "无"}</p> */}
|
||||||
|
{/* <p className="text-sm">是否使用镜像源:{app.state.useMirror ? "是" : "否"}</p> */}
|
||||||
</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">
|
<div className="w-full pt-4 border-t border-zinc-200 dark:border-zinc-800">
|
||||||
<h3 className="mb-3 text-sm font-semibold">更新检查</h3>
|
<h3 className="mb-3 text-sm font-semibold">更新检查</h3>
|
||||||
<div className="mb-3 space-y-3">
|
<div className="mb-3 space-y-3">
|
||||||
{/* <Switch
|
{/* <Switch
|
||||||
isSelected={app.state.useMirror}
|
isSelected={app.state.useMirror}
|
||||||
size="sm"
|
size="sm"
|
||||||
onChange={(e) => app.setUseMirror(e.target.checked)}
|
onChange={(e) => app.setUseMirror(e.target.checked)}
|
||||||
>
|
>
|
||||||
使用镜像源
|
使用镜像源
|
||||||
</Switch> */}
|
</Switch> */}
|
||||||
{/* <p className="text-xs text-zinc-500">
|
{/* <p className="text-xs text-zinc-500">
|
||||||
{app.state.useMirror
|
{app.state.useMirror
|
||||||
? "使用自建更新服务检查更新"
|
? "使用自建更新服务检查更新"
|
||||||
: "使用 GitHub Release 检查更新"}
|
: "使用 GitHub Release 检查更新"}
|
||||||
</p> */}
|
</p> */}
|
||||||
<Switch
|
<Switch
|
||||||
isSelected={app.state.includePrerelease}
|
isSelected={app.state.includePrerelease}
|
||||||
size="sm"
|
size="sm"
|
||||||
onChange={(e) => app.setIncludePrerelease(e.target.checked)}
|
onChange={(e) => app.setIncludePrerelease(e.target.checked)}
|
||||||
>
|
>
|
||||||
包含测试版
|
包含测试版
|
||||||
</Switch>
|
</Switch>
|
||||||
<p className="text-xs text-zinc-500">
|
<p className="text-xs text-zinc-500">
|
||||||
{app.state.includePrerelease
|
{app.state.includePrerelease
|
||||||
? "检查更新时会包含预发布版本(beta、alpha等)"
|
? "检查更新时会包含预发布版本(beta、alpha等)"
|
||||||
: "仅检查正式版本"}
|
: "仅检查正式版本"}
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
<UpdateChecker
|
||||||
|
customEndpoint={customEndpoint || undefined}
|
||||||
|
includePrerelease={app.state.includePrerelease}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<UpdateChecker
|
|
||||||
useMirror={app.state.useMirror}
|
|
||||||
customEndpoint={customEndpoint || undefined}
|
|
||||||
includePrerelease={app.state.includePrerelease}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col w-full pt-4 space-y-3 border-t border-zinc-200 dark:border-zinc-800">
|
<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>
|
<h3 className="mb-3 text-sm font-semibold">启动设置</h3>
|
||||||
<Switch
|
<Switch
|
||||||
isSelected={app.state.autoStart}
|
isSelected={app.state.autoStart}
|
||||||
size="sm"
|
size="sm"
|
||||||
onChange={(e) => app.setAutoStart(e.target.checked)}
|
onChange={(e) => app.setAutoStart(e.target.checked)}
|
||||||
>
|
>
|
||||||
开机自启动
|
开机自启动
|
||||||
</Switch>
|
</Switch>
|
||||||
<Switch
|
<Switch
|
||||||
isSelected={app.state.startHidden}
|
isSelected={app.state.startHidden}
|
||||||
size="sm"
|
size="sm"
|
||||||
onChange={(e) => app.setStartHidden(e.target.checked)}
|
onChange={(e) => app.setStartHidden(e.target.checked)}
|
||||||
>
|
>
|
||||||
静默启动
|
静默启动
|
||||||
</Switch>
|
</Switch>
|
||||||
<Switch
|
<Switch
|
||||||
isSelected={app.state.hiddenOnClose}
|
isSelected={app.state.hiddenOnClose}
|
||||||
size="sm"
|
size="sm"
|
||||||
onChange={(e) => app.setHiddenOnClose(e.target.checked)}
|
onChange={(e) => app.setHiddenOnClose(e.target.checked)}
|
||||||
>
|
>
|
||||||
关闭时最小化到托盘
|
关闭时最小化到托盘
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,13 +26,11 @@ interface UpdateInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateCheckerProps {
|
interface UpdateCheckerProps {
|
||||||
useMirror?: boolean
|
|
||||||
customEndpoint?: string
|
customEndpoint?: string
|
||||||
includePrerelease?: boolean
|
includePrerelease?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UpdateChecker({
|
export function UpdateChecker({
|
||||||
useMirror = true,
|
|
||||||
customEndpoint,
|
customEndpoint,
|
||||||
includePrerelease = false,
|
includePrerelease = false,
|
||||||
}: UpdateCheckerProps) {
|
}: UpdateCheckerProps) {
|
||||||
@@ -41,7 +39,6 @@ export function UpdateChecker({
|
|||||||
const [downloading, setDownloading] = useState(false)
|
const [downloading, setDownloading] = useState(false)
|
||||||
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null)
|
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null)
|
||||||
const [downloadProgress, setDownloadProgress] = useState(0)
|
const [downloadProgress, setDownloadProgress] = useState(0)
|
||||||
const [installerPath, setInstallerPath] = useState<string | null>(null)
|
|
||||||
const [downloadCompleted, setDownloadCompleted] = useState(false)
|
const [downloadCompleted, setDownloadCompleted] = useState(false)
|
||||||
const {
|
const {
|
||||||
isOpen: isChangelogOpen,
|
isOpen: isChangelogOpen,
|
||||||
@@ -72,16 +69,15 @@ export function UpdateChecker({
|
|||||||
setChecking(true)
|
setChecking(true)
|
||||||
setUpdateInfo(null)
|
setUpdateInfo(null)
|
||||||
setDownloadProgress(0)
|
setDownloadProgress(0)
|
||||||
setInstallerPath(null)
|
|
||||||
setDownloading(false)
|
setDownloading(false)
|
||||||
setDownloadCompleted(false)
|
setDownloadCompleted(false)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 如果有自定义端点,使用自定义端点;否则使用默认端点(GitHub Release 或镜像源)
|
// 如果有自定义端点,使用自定义端点;否则使用默认端点(GitHub Release 或镜像源)
|
||||||
const endpoint = customEndpoint || null
|
const endpoint = customEndpoint || null
|
||||||
|
|
||||||
const result = await invoke<UpdateInfo | null>("check_app_update", {
|
const result = await invoke<UpdateInfo | null>("check_app_update", {
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
useMirror: useMirror,
|
|
||||||
includePrerelease: includePrerelease,
|
includePrerelease: includePrerelease,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -105,7 +101,6 @@ export function UpdateChecker({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("检查更新失败:", error)
|
|
||||||
addToast({
|
addToast({
|
||||||
title: "检查更新失败",
|
title: "检查更新失败",
|
||||||
description: String(error),
|
description: String(error),
|
||||||
@@ -118,24 +113,19 @@ export function UpdateChecker({
|
|||||||
|
|
||||||
// 下载更新
|
// 下载更新
|
||||||
const handleDownloadUpdate = async () => {
|
const handleDownloadUpdate = async () => {
|
||||||
if (!updateInfo) return
|
if (!updateInfo) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setDownloading(true)
|
setDownloading(true)
|
||||||
setDownloadProgress(0)
|
setDownloadProgress(0)
|
||||||
setDownloadCompleted(false)
|
setDownloadCompleted(false)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 如果使用镜像源,给下载链接前面套一个 CDN 加速
|
// 使用官方 updater 插件,不需要传递 downloadUrl
|
||||||
let downloadUrl = updateInfo.download_url
|
await invoke("download_app_update")
|
||||||
if (useMirror) {
|
|
||||||
downloadUrl = `https://cdn.upup.cool/${downloadUrl}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = await invoke<string>("download_app_update", {
|
// 下载完成,标记状态
|
||||||
downloadUrl: downloadUrl,
|
|
||||||
})
|
|
||||||
|
|
||||||
setInstallerPath(path)
|
|
||||||
setDownloadProgress(100)
|
setDownloadProgress(100)
|
||||||
setDownloading(false)
|
setDownloading(false)
|
||||||
setDownloadCompleted(true)
|
setDownloadCompleted(true)
|
||||||
@@ -146,7 +136,6 @@ export function UpdateChecker({
|
|||||||
color: "success",
|
color: "success",
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("下载更新失败:", error)
|
|
||||||
const errorMsg = String(error)
|
const errorMsg = String(error)
|
||||||
if (errorMsg.includes("取消")) {
|
if (errorMsg.includes("取消")) {
|
||||||
addToast({
|
addToast({
|
||||||
@@ -178,33 +167,32 @@ export function UpdateChecker({
|
|||||||
color: "default",
|
color: "default",
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("取消下载失败:", error)
|
// 静默处理错误
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 安装更新
|
// 安装更新
|
||||||
const handleInstallUpdate = async () => {
|
const handleInstallUpdate = async () => {
|
||||||
if (!installerPath) return
|
if (!downloadCompleted) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
addToast({
|
addToast({
|
||||||
title: "安装已启动",
|
title: "安装已启动",
|
||||||
description: "安装完成后请手动重启应用",
|
description: "应用将自动重启",
|
||||||
color: "success",
|
color: "success",
|
||||||
})
|
})
|
||||||
|
|
||||||
// 调用安装命令(这会阻塞直到安装完成)
|
// 使用官方 updater 插件,不需要传递 installerPath
|
||||||
await invoke("install_app_update", {
|
// 在 Windows 上,应用会自动退出以安装更新
|
||||||
installerPath: installerPath,
|
await invoke("install_app_update")
|
||||||
})
|
|
||||||
|
|
||||||
// 安装完成后,等待一小段时间确保安装程序完全退出
|
// 在非 Windows 平台上,可能需要手动重启
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2500))
|
// Windows 上会自动退出,所以这里不需要 relaunch
|
||||||
|
// 等待一小段时间确保安装程序启动
|
||||||
// 启动新版本
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
await relaunch()
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("安装更新失败:", error)
|
|
||||||
addToast({
|
addToast({
|
||||||
title: "安装失败",
|
title: "安装失败",
|
||||||
description: String(error),
|
description: String(error),
|
||||||
@@ -230,7 +218,7 @@ export function UpdateChecker({
|
|||||||
|
|
||||||
{updateInfo && (
|
{updateInfo && (
|
||||||
<>
|
<>
|
||||||
{!downloading && !installerPath && (
|
{!downloading && !downloadCompleted && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -256,7 +244,7 @@ export function UpdateChecker({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{installerPath && (
|
{downloadCompleted && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|||||||
Reference in New Issue
Block a user