代理修改
@@ -357,3 +357,136 @@ appStore.toggleModule(moduleId, enabled)
|
|||||||
- **瀑布流布局避免卡片等高撑开**:使用 `columns-1 md:columns-2 gap-4 [&>*]:mb-4 [&>*]:break-inside-avoid` 替代 `grid`,让卡片按内容高度自然排列。
|
- **瀑布流布局避免卡片等高撑开**:使用 `columns-1 md:columns-2 gap-4 [&>*]:mb-4 [&>*]:break-inside-avoid` 替代 `grid`,让卡片按内容高度自然排列。
|
||||||
- **模块禁用清理系统状态**:模块 `onDisable` 钩子应清理系统级副作用(如系统代理注册表项),避免模块停用后遗留导致系统异常。
|
- **模块禁用清理系统状态**:模块 `onDisable` 钩子应清理系统级副作用(如系统代理注册表项),避免模块停用后遗留导致系统异常。
|
||||||
- **应用退出清理**:在 `lib.rs` 的 `quit_app` 命令和托盘退出事件中都要调用 `cleanup_on_exit` + `stop_all`,确保任何退出路径都清理干净。
|
- **应用退出清理**:在 `lib.rs` 的 `quit_app` 命令和托盘退出事件中都要调用 `cleanup_on_exit` + `stop_all`,确保任何退出路径都清理干净。
|
||||||
|
|
||||||
|
## 近期重要变更(影响其他模块开发)
|
||||||
|
|
||||||
|
以下是最近几次改动中确立的约定和模式,开发新模块时需遵循。
|
||||||
|
|
||||||
|
### 后端:子进程启动必须隐藏控制台窗口
|
||||||
|
|
||||||
|
`process_manager.rs` 暴露了公共函数 `setup_creation_flags(cmd: &mut Command)`,Windows 上会设置 `CREATE_NO_WINDOW` 标志。
|
||||||
|
|
||||||
|
**所有用 `std::process::Command` 启动外部程序的地方都必须调用它**,否则会弹出黑色控制台窗口(即使程序是后台运行)。包括:
|
||||||
|
|
||||||
|
- `ProcessManager::start()` 启动 mihomo 等内核
|
||||||
|
- `ProcessManager::check_and_cleanup()` 崩溃重启
|
||||||
|
- 任何调用 `mihomo.exe -v`、`aria2c --version` 等查询版本的场景
|
||||||
|
- 未来下载器模块启动 aria2 时同样适用
|
||||||
|
|
||||||
|
跨平台:非 Windows 平台该函数为空实现,无需条件编译。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use crate::process_manager::setup_creation_flags;
|
||||||
|
|
||||||
|
let mut cmd = std::process::Command::new(&path);
|
||||||
|
cmd.arg("-v");
|
||||||
|
setup_creation_flags(&mut cmd); // 必须调用
|
||||||
|
cmd.stdout(Stdio::piped()).stderr(Stdio::null()).stdin(Stdio::null());
|
||||||
|
```
|
||||||
|
|
||||||
|
### 后端:长时间 wait() 不要阻塞主线程
|
||||||
|
|
||||||
|
`ProcessManager::stop()` 中 `child.kill()` 是快速的,但 `child.wait()` 可能阻塞几十到几百毫秒。**wait() 已移到后台线程**:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
thread::spawn(move || {
|
||||||
|
let _ = entry.child.wait();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
新模块若有类似的"终止外部进程"逻辑,也应遵循此模式,避免 Tauri 命令阻塞导致前端卡顿。
|
||||||
|
|
||||||
|
### 后端:内核/二进制下载用流式 + 事件推送
|
||||||
|
|
||||||
|
`mihomo_manager.rs` 的 `install_kernel` 确立了"下载二进制资源"的标准模式,未来下载器模块下载 aria2 内核时应复用:
|
||||||
|
|
||||||
|
- **流式下载**:`reqwest::Response::bytes_stream()` + `futures_util::StreamExt`,避免大文件一次性读入内存
|
||||||
|
- **进度事件**:通过 `app.emit("xxx-install-progress", progress)` 推送,事件载荷结构参考 `InstallProgress`
|
||||||
|
- **事件节流**:仅在百分比变化 ≥1% 时 emit,避免事件轰炸
|
||||||
|
- **zip 解压**:用 `zip` crate(纯 Rust),不要用 PowerShell `Expand-Archive`(有执行策略问题)
|
||||||
|
- **文件名匹配**:解压后用 `find_exe_in_dir` 查找 `.exe`(zip 内文件名可能含版本号,不是固定名字),找到后重命名为标准名
|
||||||
|
|
||||||
|
### 后端:GitHub API rate limit 规避
|
||||||
|
|
||||||
|
`check_kernel_update` 采用 **API 优先 + 重定向回退** 策略:
|
||||||
|
|
||||||
|
1. 先调 `api.github.com/.../releases/latest`(能拿完整资产列表,命名变化时更健壮)
|
||||||
|
2. 失败(403 rate limit / 网络错误)时回退到访问 `github.com/.../releases/latest`,从 302 重定向的最终 URL 提取版本号,按稳定命名规则构造下载 URL
|
||||||
|
|
||||||
|
**新模块若需要查 GitHub 最新版本,应复用此模式**,不要直接调 API(未认证 60次/小时/IP 极易超限)。
|
||||||
|
|
||||||
|
### 后端:资产命名规则适配
|
||||||
|
|
||||||
|
mihomo v1.19+ 改了 Windows 资产命名,按 CPU 微架构分级:
|
||||||
|
|
||||||
|
- 旧:`mihomo-windows-amd64-vX.X.X.zip`(已废弃)
|
||||||
|
- 新:`mihomo-windows-amd64-v3-vX.X.X.zip`(v1/v2/v3 对应 CPU level)
|
||||||
|
|
||||||
|
`fetch_latest_via_api` 中的匹配优先级:v3 标准 > v3-go124 > v3-go123 > v3 其他 > v2 > v1 > 旧命名。若未来其他内核也有类似分级,参考此优先级策略。
|
||||||
|
|
||||||
|
### 前端:Transition 内的 v-if/v-else 必须加 key
|
||||||
|
|
||||||
|
**这是 dev 模式的坑**。`<Transition mode="out-in">` 内的 `v-if`/`v-else` 分支如果缺 `:key`,Vue 3.5.x dev 模式下会触发 `__vnode` 写入竞态,导致:
|
||||||
|
|
||||||
|
- 控制台报错 `Cannot set properties of null (setting '__vnode')`
|
||||||
|
- vnode 树损坏,所有事件派发失效(按钮点击没反应)
|
||||||
|
- **build 模式不报错**(生产构建剥除了 `__vnode` instrumentation),容易漏掉
|
||||||
|
|
||||||
|
**约定**:`<Transition>` 内所有分支(v-if/v-else-if/v-else)都必须加 `:key`,即使是原生 div 也要加。
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<Transition name="fade" mode="out-in">
|
||||||
|
<ComponentA v-if="cond" key="a" />
|
||||||
|
<div v-else key="empty">占位</div>
|
||||||
|
</Transition>
|
||||||
|
```
|
||||||
|
|
||||||
|
`ModuleContainer.vue` 和 `ProxyModule.vue` 的 Progress 区块已修复,新模块开发时注意。
|
||||||
|
|
||||||
|
### 前端:模块内 Tabs 顶部固定模式
|
||||||
|
|
||||||
|
模块根容器用 `h-full overflow-hidden flex flex-col`,Tabs 用 `flex-1 min-h-0 flex flex-col`,TabsList 加 `shrink-0`,TabsContent 加 `flex-1 min-h-0 overflow-y-auto`:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<div class="h-full p-6 overflow-hidden flex flex-col">
|
||||||
|
<Tabs v-model="tab" class="flex-1 min-h-0 flex flex-col">
|
||||||
|
<TabsList class="shrink-0">...</TabsList>
|
||||||
|
<TabsContent value="x" class="flex-1 min-h-0 overflow-y-auto">...</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
这样 TabsList 固定在顶部,只有 TabsContent 滚动。`min-h-0` 是 flex 子元素 overflow 生效的关键,不能省。
|
||||||
|
|
||||||
|
### 前端:Tauri 事件监听需在 store 中管理生命周期
|
||||||
|
|
||||||
|
`proxyStore.ts` 的 `installKernel` 确立了模式:
|
||||||
|
|
||||||
|
- 监听在方法调用时注册,`finally` 块中取消
|
||||||
|
- 用模块级变量保存 `UnlistenFn`,避免重复注册
|
||||||
|
- 错误事件由后端保证 emit(前端不重复弹 toast,统一由 watch 处理)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
let progressUnlisten: UnlistenFn | null = null
|
||||||
|
const installKernel = async () => {
|
||||||
|
if (!progressUnlisten) {
|
||||||
|
progressUnlisten = await listen<Progress>('xxx-progress', (e) => {
|
||||||
|
progress.value = e.payload
|
||||||
|
})
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await invoke('xxx_command')
|
||||||
|
} finally {
|
||||||
|
if (progressUnlisten) {
|
||||||
|
progressUnlisten()
|
||||||
|
progressUnlisten = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端:窗口隐藏前 blur 焦点
|
||||||
|
|
||||||
|
TitleBar 的关闭按钮实际是 `hide()` 到托盘。webview 快速隐藏时浏览器 `mouseleave` 可能不触发,导致从托盘恢复后按钮仍显示 hover 高亮。
|
||||||
|
|
||||||
|
**修复**:`hide()` 前调用 `document.activeElement.blur()`,并监听 `onFocusChanged` 在窗口重新获得焦点时再 blur 一次。新模块若有类似的"隐藏窗口"操作(如全局快捷键隐藏),同样需要 blur。
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
"tailwindcss": "^4.3.2",
|
"tailwindcss": "^4.3.2",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
|
"vue-draggable-plus": "^0.6.1",
|
||||||
"vue-sonner": "^2.0.9",
|
"vue-sonner": "^2.0.9",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -236,6 +237,8 @@
|
|||||||
|
|
||||||
"@types/node": ["@types/node@26.1.1", "https://registry.npmmirror.com/@types/node/-/node-26.1.1.tgz", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="],
|
"@types/node": ["@types/node@26.1.1", "https://registry.npmmirror.com/@types/node/-/node-26.1.1.tgz", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="],
|
||||||
|
|
||||||
|
"@types/sortablejs": ["@types/sortablejs@1.15.9", "https://registry.npmmirror.com/@types/sortablejs/-/sortablejs-1.15.9.tgz", {}, "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ=="],
|
||||||
|
|
||||||
"@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="],
|
"@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="],
|
||||||
|
|
||||||
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@5.2.4", "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", { "peerDependencies": { "vite": "^5.0.0 || ^6.0.0", "vue": "^3.2.25" } }, "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA=="],
|
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@5.2.4", "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", { "peerDependencies": { "vite": "^5.0.0 || ^6.0.0", "vue": "^3.2.25" } }, "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA=="],
|
||||||
@@ -410,6 +413,8 @@
|
|||||||
|
|
||||||
"vue-demi": ["vue-demi@0.14.10", "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", { "peerDependencies": { "@vue/composition-api": "^1.0.0-rc.1", "vue": "^3.0.0-0 || ^2.6.0" }, "optionalPeers": ["@vue/composition-api"], "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" } }, "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg=="],
|
"vue-demi": ["vue-demi@0.14.10", "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", { "peerDependencies": { "@vue/composition-api": "^1.0.0-rc.1", "vue": "^3.0.0-0 || ^2.6.0" }, "optionalPeers": ["@vue/composition-api"], "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" } }, "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg=="],
|
||||||
|
|
||||||
|
"vue-draggable-plus": ["vue-draggable-plus@0.6.1", "https://registry.npmmirror.com/vue-draggable-plus/-/vue-draggable-plus-0.6.1.tgz", { "dependencies": { "@types/sortablejs": "^1.15.8" }, "peerDependencies": { "@vue/composition-api": "*" }, "optionalPeers": ["@vue/composition-api"] }, "sha512-FbtQ/fuoixiOfTZzG3yoPl4JAo9HJXRHmBQZFB9x2NYCh6pq0TomHf7g5MUmpaDYv+LU2n6BPq2YN9sBO+FbIg=="],
|
||||||
|
|
||||||
"vue-sonner": ["vue-sonner@2.0.9", "https://registry.npmmirror.com/vue-sonner/-/vue-sonner-2.0.9.tgz", { "peerDependencies": { "@nuxt/kit": "^4.0.3", "@nuxt/schema": "^4.0.3", "nuxt": "^4.0.3" }, "optionalPeers": ["@nuxt/kit", "@nuxt/schema", "nuxt"] }, "sha512-i6BokNlNDL93fpzNxN/LZSn6D6MzlO+i3qXt6iVZne3x1k7R46d5HlFB4P8tYydhgqOrRbIZEsnRd3kG7qGXyw=="],
|
"vue-sonner": ["vue-sonner@2.0.9", "https://registry.npmmirror.com/vue-sonner/-/vue-sonner-2.0.9.tgz", { "peerDependencies": { "@nuxt/kit": "^4.0.3", "@nuxt/schema": "^4.0.3", "nuxt": "^4.0.3" }, "optionalPeers": ["@nuxt/kit", "@nuxt/schema", "nuxt"] }, "sha512-i6BokNlNDL93fpzNxN/LZSn6D6MzlO+i3qXt6iVZne3x1k7R46d5HlFB4P8tYydhgqOrRbIZEsnRd3kG7qGXyw=="],
|
||||||
|
|
||||||
"vue-tsc": ["vue-tsc@2.2.12", "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.12.tgz", { "dependencies": { "@volar/typescript": "2.4.15", "@vue/language-core": "2.2.12" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw=="],
|
"vue-tsc": ["vue-tsc@2.2.12", "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.12.tgz", { "dependencies": { "@volar/typescript": "2.4.15", "@vue/language-core": "2.2.12" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw=="],
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.3 MiB |
@@ -8,7 +8,7 @@
|
|||||||
"name": "thing",
|
"name": "thing",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lucide/vue": "^1.24.0",
|
"@lucide/vue": "^1.25.0",
|
||||||
"@tailwindcss/vite": "^4.3.2",
|
"@tailwindcss/vite": "^4.3.2",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-autostart": "~2",
|
"@tauri-apps/plugin-autostart": "~2",
|
||||||
@@ -602,9 +602,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@lucide/vue": {
|
"node_modules/@lucide/vue": {
|
||||||
"version": "1.24.0",
|
"version": "1.25.0",
|
||||||
"resolved": "https://registry.npmmirror.com/@lucide/vue/-/vue-1.24.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@lucide/vue/-/vue-1.25.0.tgz",
|
||||||
"integrity": "sha512-5bNPX0G2YEWdUlBYk7pE8SgDg/f1mkIFpJ9vtE44pW/cwRz7Ioc0tOTESoVJAPvxIELSmYekX+XXIJMjsswNIg==",
|
"integrity": "sha512-hkEetV+v48ScIn3uwqwWQ66sI8foeP2q6OMI09GzLFH4SfvBlfe3JHYlMBdBCqFC7WRlhFsndyDn/awRKRc2OQ==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vue": ">=3.0.1"
|
"vue": ">=3.0.1"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lucide/vue": "^1.24.0",
|
"@lucide/vue": "^1.25.0",
|
||||||
"@tailwindcss/vite": "^4.3.2",
|
"@tailwindcss/vite": "^4.3.2",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-autostart": "~2",
|
"@tauri-apps/plugin-autostart": "~2",
|
||||||
|
|||||||
@@ -8,6 +8,17 @@ version = "2.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aes"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cipher",
|
||||||
|
"cpufeatures",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
@@ -47,6 +58,15 @@ version = "1.0.103"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
||||||
|
|
||||||
|
[[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 = "async-broadcast"
|
name = "async-broadcast"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
@@ -354,6 +374,25 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bzip2"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
|
||||||
|
dependencies = [
|
||||||
|
"bzip2-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bzip2-sys"
|
||||||
|
version = "0.1.13+1.0.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cairo-rs"
|
name = "cairo-rs"
|
||||||
version = "0.18.5"
|
version = "0.18.5"
|
||||||
@@ -428,6 +467,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38"
|
checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
|
"jobserver",
|
||||||
|
"libc",
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -478,6 +519,16 @@ dependencies = [
|
|||||||
"windows-link 0.2.1",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cipher"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"inout",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "combine"
|
name = "combine"
|
||||||
version = "4.6.7"
|
version = "4.6.7"
|
||||||
@@ -497,6 +548,12 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "constant_time_eq"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cookie"
|
name = "cookie"
|
||||||
version = "0.18.1"
|
version = "0.18.1"
|
||||||
@@ -566,6 +623,21 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc"
|
||||||
|
version = "3.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
|
||||||
|
dependencies = [
|
||||||
|
"crc-catalog",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc-catalog"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -684,6 +756,12 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deflate64"
|
||||||
|
version = "0.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.5.8"
|
version = "0.5.8"
|
||||||
@@ -693,6 +771,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.118",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_more"
|
name = "derive_more"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
@@ -722,6 +811,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer",
|
"block-buffer",
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1296,9 +1386,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi 5.3.0",
|
"r-efi 5.3.0",
|
||||||
"wasip2",
|
"wasip2",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1515,6 +1607,15 @@ version = "0.4.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "html5ever"
|
name = "html5ever"
|
||||||
version = "0.38.0"
|
version = "0.38.0"
|
||||||
@@ -1816,6 +1917,15 @@ dependencies = [
|
|||||||
"cfb",
|
"cfb",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inout"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipnet"
|
name = "ipnet"
|
||||||
version = "2.12.0"
|
version = "2.12.0"
|
||||||
@@ -1914,6 +2024,16 @@ dependencies = [
|
|||||||
"syn 2.0.118",
|
"syn 2.0.118",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jobserver"
|
||||||
|
version = "0.1.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.4.3",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.103"
|
version = "0.3.103"
|
||||||
@@ -2043,6 +2163,27 @@ version = "0.4.33"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lzma-rs"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"crc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lzma-sys"
|
||||||
|
version = "0.1.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markup5ever"
|
name = "markup5ever"
|
||||||
version = "0.38.0"
|
version = "0.38.0"
|
||||||
@@ -2526,6 +2667,16 @@ dependencies = [
|
|||||||
"windows-link 0.2.1",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pbkdf2"
|
||||||
|
version = "0.12.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
"hmac",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.2"
|
version = "2.3.2"
|
||||||
@@ -2870,6 +3021,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
"h2",
|
"h2",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
@@ -2891,12 +3043,14 @@ dependencies = [
|
|||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
|
"wasm-streams 0.4.2",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2930,7 +3084,7 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"wasm-streams",
|
"wasm-streams 0.5.0",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3331,6 +3485,17 @@ dependencies = [
|
|||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha1"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.9"
|
version = "0.10.9"
|
||||||
@@ -3917,6 +4082,7 @@ name = "thing"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"futures-util",
|
||||||
"reqwest 0.12.28",
|
"reqwest 0.12.28",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -3925,8 +4091,10 @@ dependencies = [
|
|||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-autostart",
|
"tauri-plugin-autostart",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
|
"tokio",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
"winreg 0.52.0",
|
"winreg 0.52.0",
|
||||||
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4553,6 +4721,19 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-streams"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-streams"
|
name = "wasm-streams"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -5235,6 +5416,15 @@ dependencies = [
|
|||||||
"pkg-config",
|
"pkg-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xz2"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
|
||||||
|
dependencies = [
|
||||||
|
"lzma-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
@@ -5345,6 +5535,20 @@ name = "zeroize"
|
|||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
||||||
|
dependencies = [
|
||||||
|
"zeroize_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize_derive"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.118",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
@@ -5379,12 +5583,82 @@ dependencies = [
|
|||||||
"syn 2.0.118",
|
"syn 2.0.118",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zip"
|
||||||
|
version = "2.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
|
||||||
|
dependencies = [
|
||||||
|
"aes",
|
||||||
|
"arbitrary",
|
||||||
|
"bzip2",
|
||||||
|
"constant_time_eq",
|
||||||
|
"crc32fast",
|
||||||
|
"crossbeam-utils",
|
||||||
|
"deflate64",
|
||||||
|
"displaydoc",
|
||||||
|
"flate2",
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
"hmac",
|
||||||
|
"indexmap 2.14.0",
|
||||||
|
"lzma-rs",
|
||||||
|
"memchr",
|
||||||
|
"pbkdf2",
|
||||||
|
"sha1",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"time",
|
||||||
|
"xz2",
|
||||||
|
"zeroize",
|
||||||
|
"zopfli",
|
||||||
|
"zstd",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zopfli"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"crc32fast",
|
||||||
|
"log",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd"
|
||||||
|
version = "0.13.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||||
|
dependencies = [
|
||||||
|
"zstd-safe",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd-safe"
|
||||||
|
version = "7.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||||
|
dependencies = [
|
||||||
|
"zstd-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd-sys"
|
||||||
|
version = "2.0.16+zstd.1.5.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zvariant"
|
name = "zvariant"
|
||||||
version = "5.13.0"
|
version = "5.13.0"
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
reqwest = { version = "0.12", features = ["json"] }
|
reqwest = { version = "0.12", features = ["json", "stream"] }
|
||||||
|
futures-util = "0.3"
|
||||||
|
tokio = { version = "1", features = ["io-util"] }
|
||||||
|
zip = "2"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winreg = "0.52"
|
winreg = "0.52"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.4 KiB |
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 24 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="ic_launcher_background">#fff</color>
|
|
||||||
</resources>
|
|
||||||
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 919 B |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 231 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 15 KiB |
@@ -10,7 +10,7 @@ use logger::{
|
|||||||
use mihomo_manager::{
|
use mihomo_manager::{
|
||||||
proxy_activate_profile, proxy_check_kernel_update, proxy_clear_system_proxy, proxy_close_connection, proxy_delete_profile,
|
proxy_activate_profile, proxy_check_kernel_update, proxy_clear_system_proxy, proxy_close_connection, proxy_delete_profile,
|
||||||
proxy_get_connections, proxy_get_proxies, proxy_get_settings, proxy_get_system_proxy,
|
proxy_get_connections, proxy_get_proxies, proxy_get_settings, proxy_get_system_proxy,
|
||||||
proxy_import_profile, proxy_kernel_info, proxy_patch_configs, proxy_restart, proxy_save_settings,
|
proxy_import_profile, proxy_install_kernel, proxy_kernel_info, proxy_patch_configs, proxy_restart, proxy_save_settings,
|
||||||
proxy_select_proxy, proxy_set_system_proxy, proxy_start, proxy_status, proxy_stop,
|
proxy_select_proxy, proxy_set_system_proxy, proxy_start, proxy_status, proxy_stop,
|
||||||
proxy_test_delay, proxy_update_kernel, proxy_update_profile, proxy_version, MihomoManager,
|
proxy_test_delay, proxy_update_kernel, proxy_update_profile, proxy_version, MihomoManager,
|
||||||
};
|
};
|
||||||
@@ -59,6 +59,7 @@ pub fn run() {
|
|||||||
proxy_kernel_info,
|
proxy_kernel_info,
|
||||||
proxy_check_kernel_update,
|
proxy_check_kernel_update,
|
||||||
proxy_update_kernel,
|
proxy_update_kernel,
|
||||||
|
proxy_install_kernel,
|
||||||
proxy_status,
|
proxy_status,
|
||||||
proxy_start,
|
proxy_start,
|
||||||
proxy_stop,
|
proxy_stop,
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
|
use futures_util::StreamExt;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_yaml::Value as YamlValue;
|
use serde_yaml::Value as YamlValue;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::io::{Read, Write};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::{AppHandle, Emitter, Manager};
|
||||||
use tauri::path::BaseDirectory;
|
use tauri::path::BaseDirectory;
|
||||||
|
|
||||||
use crate::process_manager::{ProcessInfo, ProcessManager, StartProcessParams};
|
use crate::process_manager::{ProcessInfo, ProcessManager, StartProcessParams, setup_creation_flags};
|
||||||
|
|
||||||
// ===================== 数据结构 =====================
|
// ===================== 数据结构 =====================
|
||||||
|
|
||||||
@@ -44,6 +46,10 @@ pub struct ProxySettings {
|
|||||||
pub auto_switch_group: String,
|
pub auto_switch_group: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub auto_switch_region: String,
|
pub auto_switch_region: String,
|
||||||
|
/// 内核下载镜像源列表(前缀拼接到 GitHub URL 前)。
|
||||||
|
/// 空字符串 = 直连 GitHub,其余为镜像站前缀(含尾斜杠)。
|
||||||
|
#[serde(default = "default_kernel_mirrors")]
|
||||||
|
pub kernel_mirrors: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_mixed_port() -> u16 { 7890 }
|
fn default_mixed_port() -> u16 { 7890 }
|
||||||
@@ -51,6 +57,15 @@ fn default_external_controller() -> String { "127.0.0.1:9090".into() }
|
|||||||
fn default_mode() -> String { "rule".into() }
|
fn default_mode() -> String { "rule".into() }
|
||||||
fn default_log_level() -> String { "info".into() }
|
fn default_log_level() -> String { "info".into() }
|
||||||
fn default_auto_switch_interval() -> u32 { 5 }
|
fn default_auto_switch_interval() -> u32 { 5 }
|
||||||
|
/// 默认镜像源:空串=直连 GitHub 优先,后续为公益镜像(按稳定性排序)
|
||||||
|
fn default_kernel_mirrors() -> Vec<String> {
|
||||||
|
vec![
|
||||||
|
String::new(),
|
||||||
|
"https://ghproxy.net/".into(),
|
||||||
|
"https://gh-proxy.com/".into(),
|
||||||
|
"https://ghfast.top/".into(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for ProxySettings {
|
impl Default for ProxySettings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
@@ -70,6 +85,7 @@ impl Default for ProxySettings {
|
|||||||
auto_switch_interval: 5,
|
auto_switch_interval: 5,
|
||||||
auto_switch_group: String::new(),
|
auto_switch_group: String::new(),
|
||||||
auto_switch_region: String::new(),
|
auto_switch_region: String::new(),
|
||||||
|
kernel_mirrors: default_kernel_mirrors(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,6 +132,19 @@ pub struct ProxyStatus {
|
|||||||
pub restart_count: u32,
|
pub restart_count: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 内核安装进度事件载荷
|
||||||
|
/// - stage: downloading | extracting | replacing | done | error
|
||||||
|
/// - percent: 0-100(无 total_bytes 时为 0,前端按 downloadedBytes 显示)
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct InstallProgress {
|
||||||
|
pub stage: String,
|
||||||
|
pub percent: u8,
|
||||||
|
pub downloaded_bytes: u64,
|
||||||
|
pub total_bytes: Option<u64>,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
// ===================== MihomoManager =====================
|
// ===================== MihomoManager =====================
|
||||||
|
|
||||||
pub struct MihomoManager {
|
pub struct MihomoManager {
|
||||||
@@ -236,9 +265,15 @@ impl MihomoManager {
|
|||||||
let path = self.kernel_path();
|
let path = self.kernel_path();
|
||||||
let exists = path.exists();
|
let exists = path.exists();
|
||||||
let version = if exists {
|
let version = if exists {
|
||||||
std::process::Command::new(&path)
|
let mut cmd = std::process::Command::new(&path);
|
||||||
.arg("-v")
|
cmd.arg("-v");
|
||||||
.output()
|
// 隐藏控制台窗口(mihomo.exe -v 也会弹窗)
|
||||||
|
setup_creation_flags(&mut cmd);
|
||||||
|
// 重定向 stdio,避免继承主进程控制台
|
||||||
|
cmd.stdout(std::process::Stdio::piped())
|
||||||
|
.stderr(std::process::Stdio::null())
|
||||||
|
.stdin(std::process::Stdio::null());
|
||||||
|
cmd.output()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||||
.and_then(|s| {
|
.and_then(|s| {
|
||||||
@@ -270,14 +305,38 @@ impl MihomoManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 检查 GitHub 上的最新 mihomo 版本
|
/// 检查 GitHub 上的最新 mihomo 版本
|
||||||
|
/// 策略:优先用 API(能拿到完整资产列表,命名变化时更健壮),
|
||||||
|
/// 失败时回退到重定向解析(不受 API rate limit 限制)
|
||||||
pub async fn check_kernel_update(&self) -> Result<KernelUpdateInfo, String> {
|
pub async fn check_kernel_update(&self) -> Result<KernelUpdateInfo, String> {
|
||||||
let resp: serde_json::Value = self
|
match self.fetch_latest_via_api().await {
|
||||||
|
Ok(info) => Ok(info),
|
||||||
|
Err(api_err) => {
|
||||||
|
eprintln!("[kernel] API 查询失败,回退到重定向解析: {}", api_err);
|
||||||
|
self.fetch_latest_via_redirect().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 通过 GitHub API 查询最新版本(受 rate limit 限制:未认证 60次/小时/IP)
|
||||||
|
async fn fetch_latest_via_api(&self) -> Result<KernelUpdateInfo, String> {
|
||||||
|
let resp = self
|
||||||
.client
|
.client
|
||||||
.get("https://api.github.com/repos/MetaCubeX/mihomo/releases/latest")
|
.get("https://api.github.com/repos/MetaCubeX/mihomo/releases/latest")
|
||||||
.header("User-Agent", "thing-app")
|
.header("User-Agent", "thing-app")
|
||||||
|
.timeout(std::time::Duration::from_secs(15))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("请求 GitHub API 失败: {}", e))?
|
.map_err(|e| format!("请求 GitHub API 失败: {}", e))?;
|
||||||
|
let status = resp.status();
|
||||||
|
if !status.is_success() {
|
||||||
|
let body = resp.text().await.unwrap_or_default();
|
||||||
|
return Err(format!(
|
||||||
|
"GitHub API 返回 HTTP {}:{}",
|
||||||
|
status.as_u16(),
|
||||||
|
if body.len() > 300 { format!("{}...", &body[..300]) } else { body }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let resp: serde_json::Value = resp
|
||||||
.json()
|
.json()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("解析 GitHub 响应失败: {}", e))?;
|
.map_err(|e| format!("解析 GitHub 响应失败: {}", e))?;
|
||||||
@@ -288,33 +347,92 @@ impl MihomoManager {
|
|||||||
.unwrap_or("unknown")
|
.unwrap_or("unknown")
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
// 查找 windows amd64 zip 资产(非 compatible 版本)
|
let assets_arr = resp.get("assets").and_then(|a| a.as_array());
|
||||||
let download_url = resp
|
|
||||||
.get("assets")
|
// 收集所有 windows amd64 zip 候选资产(排除 compatible/arm64/386)
|
||||||
.and_then(|a| a.as_array())
|
let candidates: Vec<(String, String)> = assets_arr
|
||||||
.and_then(|assets| {
|
.map(|assets| {
|
||||||
assets.iter().find_map(|asset| {
|
assets.iter().filter_map(|asset| {
|
||||||
let name = asset.get("name")?.as_str()?;
|
let name = asset.get("name")?.as_str()?;
|
||||||
let url = asset.get("browser_download_url")?.as_str()?;
|
let url = asset.get("browser_download_url")?.as_str()?;
|
||||||
// 匹配 mihomo-windows-amd64-v*.zip,排除 compatible/arm64
|
|
||||||
if name.starts_with("mihomo-windows-amd64-")
|
if name.starts_with("mihomo-windows-amd64-")
|
||||||
&& name.ends_with(".zip")
|
&& name.ends_with(".zip")
|
||||||
&& !name.contains("compatible")
|
&& !name.contains("compatible")
|
||||||
&& !name.contains("arm64")
|
&& !name.contains("arm64")
|
||||||
|
&& !name.contains("386")
|
||||||
{
|
{
|
||||||
Some(url.to_string())
|
Some((name.to_string(), url.to_string()))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
}).collect()
|
||||||
})
|
})
|
||||||
.ok_or_else(|| "未找到适用的 Windows amd64 内核资产".to_string())?;
|
.unwrap_or_default();
|
||||||
|
|
||||||
// 当前版本
|
// 按优先级匹配:v3 标准 > v3-go124 > v3-go123 > v3 其他 > v2 > v1 > 旧命名
|
||||||
|
let download_url = candidates
|
||||||
|
.iter().find(|(n, _)| n.contains("-v3-v") && !n.contains("-go"))
|
||||||
|
.or_else(|| candidates.iter().find(|(n, _)| n.contains("-v3-go124-")))
|
||||||
|
.or_else(|| candidates.iter().find(|(n, _)| n.contains("-v3-go123-")))
|
||||||
|
.or_else(|| candidates.iter().find(|(n, _)| n.contains("-v3-go")))
|
||||||
|
.or_else(|| candidates.iter().find(|(n, _)| n.contains("-v2-v")))
|
||||||
|
.or_else(|| candidates.iter().find(|(n, _)| n.contains("-v1-v")))
|
||||||
|
.or_else(|| candidates.iter().find(|(n, _)| {
|
||||||
|
!n.contains("-v1-") && !n.contains("-v2-") && !n.contains("-v3-")
|
||||||
|
}))
|
||||||
|
.map(|(_, u)| u.clone())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
let candidates_str = candidates.iter()
|
||||||
|
.map(|(n, _)| n.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
format!("API 未找到适用的 Windows amd64 内核资产。候选:[{}]", candidates_str)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(self.build_update_info(latest_version, download_url))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 通过 releases/latest 重定向解析版本号(不受 API rate limit 限制)
|
||||||
|
/// 访问 https://github.com/MetaCubeX/mihomo/releases/latest 会 302 到
|
||||||
|
/// https://github.com/MetaCubeX/mihomo/releases/tag/v1.19.13
|
||||||
|
/// 从最终 URL 提取版本号后,按 v1.19+ 稳定命名规则构造下载 URL
|
||||||
|
async fn fetch_latest_via_redirect(&self) -> Result<KernelUpdateInfo, String> {
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.get("https://github.com/MetaCubeX/mihomo/releases/latest")
|
||||||
|
.header("User-Agent", "thing-app")
|
||||||
|
.timeout(std::time::Duration::from_secs(15))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("请求 GitHub releases 页面失败: {}", e))?;
|
||||||
|
|
||||||
|
// 从重定向后的最终 URL 提取版本号
|
||||||
|
let final_url = resp.url().to_string();
|
||||||
|
let latest_version = final_url
|
||||||
|
.rsplit('/')
|
||||||
|
.next()
|
||||||
|
.filter(|s| s.starts_with('v') && s.chars().any(|c| c == '.'))
|
||||||
|
.ok_or_else(|| format!("无法从重定向 URL 提取版本号: {}", final_url))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// 构造下载 URL:mihomo v1.19+ 稳定使用 -v3-vX.X.X.zip 命名(CPU level v3)
|
||||||
|
let download_url = format!(
|
||||||
|
"https://github.com/MetaCubeX/mihomo/releases/download/{}/mihomo-windows-amd64-v3-{}.zip",
|
||||||
|
latest_version, latest_version
|
||||||
|
);
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"[kernel] 重定向解析成功: version={}, url={}",
|
||||||
|
latest_version, download_url
|
||||||
|
);
|
||||||
|
Ok(self.build_update_info(latest_version, download_url))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据最新版本和下载 URL 构造更新信息(含当前版本比较)
|
||||||
|
fn build_update_info(&self, latest_version: String, download_url: String) -> KernelUpdateInfo {
|
||||||
let current = self.kernel_info().version;
|
let current = self.kernel_info().version;
|
||||||
let has_update = match ¤t {
|
let has_update = match ¤t {
|
||||||
Some(c) => {
|
Some(c) => {
|
||||||
// 简单比较:从当前版本字符串提取版本号
|
|
||||||
let cur_ver = c
|
let cur_ver = c
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
.find(|s| s.starts_with('v') && s.chars().filter(|c| *c == '.').count() >= 2)
|
.find(|s| s.starts_with('v') && s.chars().filter(|c| *c == '.').count() >= 2)
|
||||||
@@ -323,85 +441,266 @@ impl MihomoManager {
|
|||||||
}
|
}
|
||||||
None => true,
|
None => true,
|
||||||
};
|
};
|
||||||
|
KernelUpdateInfo {
|
||||||
Ok(KernelUpdateInfo {
|
|
||||||
current_version: current,
|
current_version: current,
|
||||||
latest_version,
|
latest_version,
|
||||||
download_url,
|
download_url,
|
||||||
has_update,
|
has_update,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 下载并安装内核更新
|
/// 下载并安装内核(首次安装与更新共用此方法)
|
||||||
pub async fn update_kernel(&self) -> Result<KernelInfo, String> {
|
/// - mirror_prefix: 用户选择的镜像源前缀(空串=直连 GitHub)
|
||||||
|
/// - 流式下载:实时推送下载进度到前端
|
||||||
|
/// - zip crate 解压:替代 PowerShell,避免执行策略问题
|
||||||
|
/// - 备份旧内核:替换前备份为 .bak
|
||||||
|
/// 任何阶段失败都会 emit error 事件,避免前端进度卡在初始状态
|
||||||
|
pub async fn install_kernel(&self, app: &AppHandle, mirror_prefix: String) -> Result<KernelInfo, String> {
|
||||||
|
let result = self.install_kernel_inner(app, mirror_prefix).await;
|
||||||
|
if let Err(ref e) = result {
|
||||||
|
let _ = app.emit(
|
||||||
|
"kernel-install-progress",
|
||||||
|
InstallProgress {
|
||||||
|
stage: "error".into(),
|
||||||
|
percent: 0,
|
||||||
|
downloaded_bytes: 0,
|
||||||
|
total_bytes: None,
|
||||||
|
message: e.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn install_kernel_inner(&self, app: &AppHandle, mirror_prefix: String) -> Result<KernelInfo, String> {
|
||||||
let info = self.check_kernel_update().await?;
|
let info = self.check_kernel_update().await?;
|
||||||
let zip_path = self.cores_dir().join("mihomo-update.zip");
|
let zip_path = self.cores_dir().join("mihomo-update.zip");
|
||||||
let extract_dir = self.cores_dir().join("mihomo-update-tmp");
|
let extract_dir = self.cores_dir().join("mihomo-update-tmp");
|
||||||
|
|
||||||
// 下载 zip
|
// 拼接用户选择的镜像源 URL
|
||||||
let resp = self
|
let url = if mirror_prefix.is_empty() {
|
||||||
.client
|
info.download_url.clone()
|
||||||
.get(&info.download_url)
|
} else {
|
||||||
.header("User-Agent", "thing-app")
|
format!("{}{}", mirror_prefix, info.download_url)
|
||||||
.send()
|
};
|
||||||
.await
|
let label = if mirror_prefix.is_empty() { "GitHub 直连".to_string() } else { mirror_prefix.clone() };
|
||||||
.map_err(|e| format!("下载内核失败: {}", e))?;
|
let _ = app.emit(
|
||||||
if !resp.status().is_success() {
|
"kernel-install-progress",
|
||||||
return Err(format!("下载失败: HTTP {}", resp.status()));
|
InstallProgress {
|
||||||
}
|
stage: "downloading".into(),
|
||||||
let bytes = resp
|
percent: 0,
|
||||||
.bytes()
|
downloaded_bytes: 0,
|
||||||
.await
|
total_bytes: None,
|
||||||
.map_err(|e| format!("读取下载内容失败: {}", e))?;
|
message: format!("正在下载:{}", label),
|
||||||
fs::write(&zip_path, &bytes).map_err(|e| format!("保存 zip 失败: {}", e))?;
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// 清理旧解压目录
|
// 单源下载(用户已选择)
|
||||||
|
match self.download_with_progress(app, &url, &zip_path).await {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(e) => {
|
||||||
|
let msg = format!("下载失败({}):{}", label, e);
|
||||||
|
let _ = app.emit(
|
||||||
|
"kernel-install-progress",
|
||||||
|
InstallProgress {
|
||||||
|
stage: "error".into(),
|
||||||
|
percent: 0,
|
||||||
|
downloaded_bytes: 0,
|
||||||
|
total_bytes: None,
|
||||||
|
message: msg.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let _ = fs::remove_file(&zip_path);
|
||||||
|
return Err(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解压阶段
|
||||||
|
let _ = app.emit(
|
||||||
|
"kernel-install-progress",
|
||||||
|
InstallProgress {
|
||||||
|
stage: "extracting".into(),
|
||||||
|
percent: 92,
|
||||||
|
downloaded_bytes: 0,
|
||||||
|
total_bytes: None,
|
||||||
|
message: "正在解压...".into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
if extract_dir.exists() {
|
if extract_dir.exists() {
|
||||||
fs::remove_dir_all(&extract_dir).ok();
|
fs::remove_dir_all(&extract_dir).ok();
|
||||||
}
|
}
|
||||||
fs::create_dir_all(&extract_dir).map_err(|e| e.to_string())?;
|
fs::create_dir_all(&extract_dir).map_err(|e| e.to_string())?;
|
||||||
|
if let Err(e) = self.extract_zip(&zip_path, &extract_dir) {
|
||||||
// 用 PowerShell 解压
|
let _ = app.emit(
|
||||||
let output = std::process::Command::new("powershell")
|
"kernel-install-progress",
|
||||||
.args([
|
InstallProgress {
|
||||||
"-NoProfile",
|
stage: "error".into(),
|
||||||
"-Command",
|
percent: 0,
|
||||||
&format!(
|
downloaded_bytes: 0,
|
||||||
"Expand-Archive -Path '{}' -DestinationPath '{}' -Force",
|
total_bytes: None,
|
||||||
zip_path.to_string_lossy(),
|
message: format!("解压失败:{}", e),
|
||||||
extract_dir.to_string_lossy()
|
},
|
||||||
),
|
);
|
||||||
])
|
return Err(e);
|
||||||
.output()
|
|
||||||
.map_err(|e| format!("解压失败: {}", e))?;
|
|
||||||
if !output.status.success() {
|
|
||||||
let err = String::from_utf8_lossy(&output.stderr);
|
|
||||||
return Err(format!("解压失败: {}", err));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找解压出的 mihomo.exe
|
// 在解压目录中递归查找 exe 文件
|
||||||
let new_exe = extract_dir.join("mihomo.exe");
|
// mihomo zip 内的 exe 名字通常与 zip 同名(如 mihomo-windows-amd64-v3-v1.19.13.exe),
|
||||||
if !new_exe.exists() {
|
// 不是固定的 mihomo.exe,所以查找唯一的 .exe 文件即可
|
||||||
// 可能在不同子目录
|
let new_exe = self
|
||||||
return Err("解压后未找到 mihomo.exe".into());
|
.find_exe_in_dir(&extract_dir)
|
||||||
}
|
.ok_or_else(|| "解压后未找到任何 .exe 文件".to_string())?;
|
||||||
|
|
||||||
// 备份旧内核
|
// 替换阶段
|
||||||
|
let _ = app.emit(
|
||||||
|
"kernel-install-progress",
|
||||||
|
InstallProgress {
|
||||||
|
stage: "replacing".into(),
|
||||||
|
percent: 96,
|
||||||
|
downloaded_bytes: 0,
|
||||||
|
total_bytes: None,
|
||||||
|
message: "正在安装...".into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
let kernel = self.kernel_path();
|
let kernel = self.kernel_path();
|
||||||
if kernel.exists() {
|
if kernel.exists() {
|
||||||
let bak = self.cores_dir().join("mihomo.exe.bak");
|
let bak = self.cores_dir().join("mihomo.exe.bak");
|
||||||
fs::remove_file(&bak).ok();
|
fs::remove_file(&bak).ok();
|
||||||
fs::rename(&kernel, &bak).map_err(|e| format!("备份旧内核失败: {}", e))?;
|
fs::rename(&kernel, &bak).map_err(|e| format!("备份旧内核失败: {}", e))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移动新内核
|
|
||||||
fs::rename(&new_exe, &kernel).map_err(|e| format!("替换内核失败: {}", e))?;
|
fs::rename(&new_exe, &kernel).map_err(|e| format!("替换内核失败: {}", e))?;
|
||||||
|
|
||||||
// 清理临时文件
|
// 清理临时文件
|
||||||
fs::remove_file(&zip_path).ok();
|
fs::remove_file(&zip_path).ok();
|
||||||
fs::remove_dir_all(&extract_dir).ok();
|
fs::remove_dir_all(&extract_dir).ok();
|
||||||
|
|
||||||
Ok(self.kernel_info())
|
let final_info = self.kernel_info();
|
||||||
|
let _ = app.emit(
|
||||||
|
"kernel-install-progress",
|
||||||
|
InstallProgress {
|
||||||
|
stage: "done".into(),
|
||||||
|
percent: 100,
|
||||||
|
downloaded_bytes: 0,
|
||||||
|
total_bytes: None,
|
||||||
|
message: format!(
|
||||||
|
"安装完成 ({})",
|
||||||
|
final_info.version.as_deref().unwrap_or("unknown")
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Ok(final_info)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 流式下载并实时推送进度事件
|
||||||
|
async fn download_with_progress(
|
||||||
|
&self,
|
||||||
|
app: &AppHandle,
|
||||||
|
url: &str,
|
||||||
|
dest: &PathBuf,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.get(url)
|
||||||
|
.header("User-Agent", "thing-app")
|
||||||
|
.timeout(std::time::Duration::from_secs(30))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("请求失败: {}", e))?;
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
return Err(format!("HTTP {}", resp.status()));
|
||||||
|
}
|
||||||
|
let total = resp.content_length();
|
||||||
|
let mut stream = resp.bytes_stream();
|
||||||
|
let mut file = fs::File::create(dest).map_err(|e| format!("创建文件失败: {}", e))?;
|
||||||
|
let mut downloaded: u64 = 0;
|
||||||
|
let mut last_percent: u8 = 0;
|
||||||
|
while let Some(chunk) = stream.next().await {
|
||||||
|
let chunk = chunk.map_err(|e| format!("读取流失败: {}", e))?;
|
||||||
|
file.write_all(&chunk).map_err(|e| format!("写入文件失败: {}", e))?;
|
||||||
|
downloaded += chunk.len() as u64;
|
||||||
|
// 下载占总进度的 0-90%
|
||||||
|
let percent = match total {
|
||||||
|
Some(t) if t > 0 => ((downloaded as f64 / t as f64) * 90.0) as u8,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
// 仅在变化超过 1% 时 emit,避免事件轰炸
|
||||||
|
if percent >= last_percent + 1 {
|
||||||
|
last_percent = percent;
|
||||||
|
let _ = app.emit(
|
||||||
|
"kernel-install-progress",
|
||||||
|
InstallProgress {
|
||||||
|
stage: "downloading".into(),
|
||||||
|
percent,
|
||||||
|
downloaded_bytes: downloaded,
|
||||||
|
total_bytes: total,
|
||||||
|
message: format!("已下载 {:.2} MB", downloaded as f64 / 1024.0 / 1024.0),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file.flush().map_err(|e| format!("flush 失败: {}", e))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 用 zip crate 解压(纯 Rust,避免 PowerShell 执行策略问题)
|
||||||
|
fn extract_zip(&self, zip_path: &PathBuf, dest: &PathBuf) -> Result<(), String> {
|
||||||
|
let file = fs::File::open(zip_path).map_err(|e| format!("打开 zip 失败: {}", e))?;
|
||||||
|
let mut archive = zip::ZipArchive::new(file).map_err(|e| format!("读取 zip 失败: {}", e))?;
|
||||||
|
for i in 0..archive.len() {
|
||||||
|
let mut entry = archive
|
||||||
|
.by_index(i)
|
||||||
|
.map_err(|e| format!("读取条目失败: {}", e))?;
|
||||||
|
let outpath = match entry.enclosed_name() {
|
||||||
|
Some(p) => dest.join(p),
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
if entry.is_dir() {
|
||||||
|
fs::create_dir_all(&outpath).map_err(|e| e.to_string())?;
|
||||||
|
} else {
|
||||||
|
if let Some(parent) = outpath.parent() {
|
||||||
|
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
let mut outfile = fs::File::create(&outpath).map_err(|e| e.to_string())?;
|
||||||
|
let mut buf = [0u8; 8192];
|
||||||
|
loop {
|
||||||
|
let n = entry.read(&mut buf).map_err(|e| e.to_string())?;
|
||||||
|
if n == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
outfile.write_all(&buf[..n]).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 递归查找目录中的 .exe 文件
|
||||||
|
/// mihomo zip 内的 exe 名字不固定(可能含版本号、CPU level 等),
|
||||||
|
/// 策略:收集所有 .exe,优先返回名字含 "mihomo" 的,否则返回第一个
|
||||||
|
fn find_exe_in_dir(&self, dir: &PathBuf) -> Option<PathBuf> {
|
||||||
|
let mut exes: Vec<PathBuf> = Vec::new();
|
||||||
|
self.collect_exes(dir, &mut exes);
|
||||||
|
if exes.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// 优先选名字含 mihomo 的
|
||||||
|
exes.iter()
|
||||||
|
.find(|p| p.file_name().and_then(|n| n.to_str()).map(|s| s.to_lowercase().contains("mihomo")).unwrap_or(false))
|
||||||
|
.or_else(|| exes.first())
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_exes(&self, dir: &PathBuf, out: &mut Vec<PathBuf>) {
|
||||||
|
if let Ok(entries) = fs::read_dir(dir) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
self.collect_exes(&path, out);
|
||||||
|
} else if path.extension().and_then(|e| e.to_str()).map(|s| s.eq_ignore_ascii_case("exe")).unwrap_or(false) {
|
||||||
|
out.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 配置生成 ----------
|
// ---------- 配置生成 ----------
|
||||||
@@ -848,8 +1147,22 @@ pub async fn proxy_check_kernel_update(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn proxy_update_kernel(state: tauri::State<'_, MihomoManager>) -> Result<KernelInfo, String> {
|
pub async fn proxy_update_kernel(
|
||||||
state.update_kernel().await
|
state: tauri::State<'_, MihomoManager>,
|
||||||
|
app: AppHandle,
|
||||||
|
mirror_prefix: Option<String>,
|
||||||
|
) -> Result<KernelInfo, String> {
|
||||||
|
state.install_kernel(&app, mirror_prefix.unwrap_or_default()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 首次安装内核(与 update_kernel 共用 install_kernel 实现,语义独立便于前端区分场景)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn proxy_install_kernel(
|
||||||
|
state: tauri::State<'_, MihomoManager>,
|
||||||
|
app: AppHandle,
|
||||||
|
mirror_prefix: Option<String>,
|
||||||
|
) -> Result<KernelInfo, String> {
|
||||||
|
state.install_kernel(&app, mirror_prefix.unwrap_or_default()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -6,6 +6,24 @@ use std::thread;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tauri::{AppHandle, Emitter, Manager};
|
use tauri::{AppHandle, Emitter, Manager};
|
||||||
|
|
||||||
|
// Windows 平台用于隐藏控制台窗口的标志位
|
||||||
|
// CREATE_NO_WINDOW = 0x08000000,阻止子进程创建新的控制台窗口
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||||
|
|
||||||
|
/// 为 Command 设置平台特定的创建标志(Windows 上隐藏控制台窗口)
|
||||||
|
/// 公开以便其他模块(如 mihomo_manager 调用 mihomo -v 查询版本)复用
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub fn setup_creation_flags(cmd: &mut Command) {
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
pub fn setup_creation_flags(_cmd: &mut Command) {
|
||||||
|
// 非 Windows 平台无需处理
|
||||||
|
}
|
||||||
|
|
||||||
/// 进程状态枚举
|
/// 进程状态枚举
|
||||||
#[derive(Serialize, Clone, Debug)]
|
#[derive(Serialize, Clone, Debug)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
@@ -94,6 +112,8 @@ impl ProcessManager {
|
|||||||
cmd.stdout(Stdio::null())
|
cmd.stdout(Stdio::null())
|
||||||
.stderr(Stdio::null())
|
.stderr(Stdio::null())
|
||||||
.stdin(Stdio::null());
|
.stdin(Stdio::null());
|
||||||
|
// Windows 上隐藏控制台窗口(mihomo.exe 是控制台程序,否则会弹黑框)
|
||||||
|
setup_creation_flags(&mut cmd);
|
||||||
|
|
||||||
let child = cmd
|
let child = cmd
|
||||||
.spawn()
|
.spawn()
|
||||||
@@ -123,6 +143,8 @@ impl ProcessManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 停止指定进程
|
/// 停止指定进程
|
||||||
|
/// kill 在主线程执行(快速),wait 移到后台线程执行避免阻塞前端
|
||||||
|
/// Windows 上 kill 后 wait 可能需要等待子进程清理资源,有几十毫秒到几百毫秒延迟
|
||||||
pub fn stop(&self, id: &str) -> Result<(), String> {
|
pub fn stop(&self, id: &str) -> Result<(), String> {
|
||||||
let mut processes = self.processes.lock().map_err(|e| e.to_string())?;
|
let mut processes = self.processes.lock().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
@@ -131,7 +153,11 @@ impl ProcessManager {
|
|||||||
.child
|
.child
|
||||||
.kill()
|
.kill()
|
||||||
.map_err(|e| format!("终止进程 '{}' 失败: {}", id, e))?;
|
.map_err(|e| format!("终止进程 '{}' 失败: {}", id, e))?;
|
||||||
let _ = entry.child.wait();
|
// 后台等待子进程退出,避免阻塞当前调用线程(前端会感知卡顿)
|
||||||
|
// child 已 move 进闭包,wait 在后台完成
|
||||||
|
thread::spawn(move || {
|
||||||
|
let _ = entry.child.wait();
|
||||||
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(format!("进程 '{}' 不存在", id))
|
Err(format!("进程 '{}' 不存在", id))
|
||||||
@@ -240,6 +266,7 @@ impl ProcessManager {
|
|||||||
cmd.stdout(Stdio::null())
|
cmd.stdout(Stdio::null())
|
||||||
.stderr(Stdio::null())
|
.stderr(Stdio::null())
|
||||||
.stdin(Stdio::null());
|
.stdin(Stdio::null());
|
||||||
|
setup_creation_flags(&mut cmd);
|
||||||
|
|
||||||
match cmd.spawn() {
|
match cmd.spawn() {
|
||||||
Ok(new_child) => {
|
Ok(new_child) => {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ defineProps<{
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
|
key="empty"
|
||||||
class="h-full w-full flex items-center justify-center text-muted-foreground"
|
class="h-full w-full flex items-center justify-center text-muted-foreground"
|
||||||
>
|
>
|
||||||
<p>未找到模块</p>
|
<p>未找到模块</p>
|
||||||
|
|||||||
@@ -65,10 +65,43 @@ const maximize = async () => {
|
|||||||
await tauriWindow?.toggleMaximize()
|
await tauriWindow?.toggleMaximize()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hover 抑制标志:窗口隐藏时设为 true,重新显示后短暂保持 true 再恢复
|
||||||
|
// 期间用 CSS 覆盖 :hover 样式,避免按钮残留高亮(webview 隐藏时 mouseleave 不触发)
|
||||||
|
const hoverSuppressed = ref(true)
|
||||||
|
|
||||||
const close = async () => {
|
const close = async () => {
|
||||||
|
// 隐藏前立即抑制 hover,避免冻结的 :hover 状态残留到下次显示
|
||||||
|
hoverSuppressed.value = true
|
||||||
|
if (document.activeElement instanceof HTMLElement) {
|
||||||
|
document.activeElement.blur()
|
||||||
|
}
|
||||||
await tauriWindow?.hide()
|
await tauriWindow?.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 窗口重新获得焦点时,先保持抑制(防止冻结的 hover 显示),
|
||||||
|
// 短暂延迟后恢复 hover(让真实鼠标位置重新接管)
|
||||||
|
if (tauriWindow) {
|
||||||
|
tauriWindow.onFocusChanged(({ payload: focused }) => {
|
||||||
|
if (focused) {
|
||||||
|
hoverSuppressed.value = true
|
||||||
|
if (document.activeElement instanceof HTMLElement) {
|
||||||
|
document.activeElement.blur()
|
||||||
|
}
|
||||||
|
// 100ms 后恢复 hover,足够让浏览器重置伪类状态
|
||||||
|
setTimeout(() => {
|
||||||
|
hoverSuppressed.value = false
|
||||||
|
}, 100)
|
||||||
|
} else {
|
||||||
|
hoverSuppressed.value = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始时窗口已显示,恢复 hover
|
||||||
|
setTimeout(() => {
|
||||||
|
hoverSuppressed.value = false
|
||||||
|
}, 200)
|
||||||
|
|
||||||
const handleBlur = () => {
|
const handleBlur = () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isSearchFocused.value = false
|
isSearchFocused.value = false
|
||||||
@@ -147,7 +180,7 @@ const handleBlur = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center pointer-events-auto">
|
<div class="flex items-center pointer-events-auto" :class="{ 'hover-suppressed': hoverSuppressed }">
|
||||||
<button
|
<button
|
||||||
class="h-10 w-10 flex items-center justify-center hover:bg-secondary/50 transition-colors rounded-sm"
|
class="h-10 w-10 flex items-center justify-center hover:bg-secondary/50 transition-colors rounded-sm"
|
||||||
@click="minimize"
|
@click="minimize"
|
||||||
@@ -172,3 +205,10 @@ const handleBlur = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 窗口隐藏/重新显示瞬间,抑制按钮 hover 样式,避免冻结的 :hover 残留 */
|
||||||
|
.hover-suppressed button:hover {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { ProgressRootProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import {
|
||||||
|
ProgressIndicator,
|
||||||
|
ProgressRoot,
|
||||||
|
} from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<ProgressRootProps & { class?: HTMLAttributes["class"] }>(),
|
||||||
|
{
|
||||||
|
modelValue: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ProgressRoot
|
||||||
|
data-slot="progress"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'bg-primary/20 relative h-2 w-full overflow-hidden rounded-full',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<ProgressIndicator
|
||||||
|
data-slot="progress-indicator"
|
||||||
|
class="bg-primary h-full w-full flex-1 transition-all"
|
||||||
|
:style="`transform: translateX(-${100 - (props.modelValue ?? 0)}%);`"
|
||||||
|
/>
|
||||||
|
</ProgressRoot>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as Progress } from "./Progress.vue"
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import {
|
import {
|
||||||
Globe, Play, Square, RotateCw, Power, Zap, Plus, RefreshCw, Trash2,
|
Globe, Play, Square, RotateCw, Power, Zap, Plus, RefreshCw, Trash2,
|
||||||
Check, AlertCircle, Server, Settings as SettingsIcon, ListChecks,
|
Check, AlertCircle, Server, Settings as SettingsIcon, ListChecks,
|
||||||
Upload, Link2, Loader2, Download, Timer, Target, FolderOpen, Copy
|
Upload, Link2, Loader2, Download, Timer, Target, FolderOpen, Copy, DownloadCloud
|
||||||
} from '@lucide/vue'
|
} from '@lucide/vue'
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
@@ -21,9 +21,8 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
|
|||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/ui/accordion'
|
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/ui/accordion'
|
||||||
import {
|
import { Progress } from '@/components/ui/progress'
|
||||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
} from '@/components/ui/select'
|
|
||||||
import {
|
import {
|
||||||
AlertDialog, AlertDialogAction, AlertDialogCancel,
|
AlertDialog, AlertDialogAction, AlertDialogCancel,
|
||||||
AlertDialogContent, AlertDialogDescription, AlertDialogFooter,
|
AlertDialogContent, AlertDialogDescription, AlertDialogFooter,
|
||||||
@@ -573,6 +572,97 @@ const handleUpdateKernel = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 首次安装内核 =====
|
||||||
|
const installStageText = computed(() => {
|
||||||
|
const stage = store.installProgress?.stage
|
||||||
|
switch (stage) {
|
||||||
|
case 'downloading': return '正在下载'
|
||||||
|
case 'extracting': return '正在解压'
|
||||||
|
case 'replacing': return '正在安装'
|
||||||
|
case 'done': return '安装完成'
|
||||||
|
case 'error': return '安装失败'
|
||||||
|
default: return ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const installStageColor = computed(() => {
|
||||||
|
const stage = store.installProgress?.stage
|
||||||
|
if (stage === 'done') return 'text-emerald-500'
|
||||||
|
if (stage === 'error') return 'text-destructive'
|
||||||
|
return 'text-primary'
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 进度条显示百分比:有 totalBytes 时用 percent,否则显示已下载 MB 而不显示百分比 */
|
||||||
|
const installPercentDisplay = computed(() => {
|
||||||
|
const p = store.installProgress
|
||||||
|
if (!p) return 0
|
||||||
|
// downloading 阶段用 percent(后端按 0-90 计算)
|
||||||
|
// extracting=92 / replacing=96 / done=100 / error=0
|
||||||
|
if (p.stage === 'downloading' && !p.totalBytes) {
|
||||||
|
// 无总长度时,前端不可知百分比,进度条用 indeterminate 动画
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return p.percent
|
||||||
|
})
|
||||||
|
|
||||||
|
const installHasTotal = computed(() => store.installProgress?.totalBytes != null)
|
||||||
|
|
||||||
|
const formatMB = (bytes: number) => `${(bytes / 1024 / 1024).toFixed(2)} MB`
|
||||||
|
|
||||||
|
// ===== 首次安装内核 =====
|
||||||
|
// 镜像源选择:'__direct' = GitHub 直连(value 不能用空串,reka-ui SelectItem 禁止空 value)
|
||||||
|
// | 'ghproxy.net' 等预设 key | '__custom' = 自定义
|
||||||
|
const MIRROR_PRESETS = [
|
||||||
|
{ label: 'GitHub 直连', value: '__direct', hint: '需能访问 GitHub,速度最快' },
|
||||||
|
{ label: 'ghproxy.net', value: 'https://ghproxy.net/', hint: '老牌公益镜像' },
|
||||||
|
{ label: 'gh-proxy.com', value: 'https://gh-proxy.com/', hint: '公益镜像' },
|
||||||
|
{ label: 'ghfast.top', value: 'https://ghfast.top/', hint: '较新镜像' },
|
||||||
|
{ label: '自定义', value: '__custom', hint: '手动输入镜像站前缀' },
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const mirrorChoice = ref<string>('__direct') // 默认直连
|
||||||
|
const customMirror = ref<string>('')
|
||||||
|
|
||||||
|
const selectedMirrorPrefix = computed(() => {
|
||||||
|
if (mirrorChoice.value === '__custom') {
|
||||||
|
// 自定义:保证以 / 结尾,避免拼接错误
|
||||||
|
const v = customMirror.value.trim()
|
||||||
|
if (!v) return ''
|
||||||
|
return v.endsWith('/') ? v : v + '/'
|
||||||
|
}
|
||||||
|
// __direct 映射回空串(后端用空串表示直连)
|
||||||
|
if (mirrorChoice.value === '__direct') return ''
|
||||||
|
return mirrorChoice.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleInstallKernel = async () => {
|
||||||
|
if (store.installing) return
|
||||||
|
try {
|
||||||
|
await store.installKernel(selectedMirrorPrefix.value)
|
||||||
|
} catch {
|
||||||
|
// 忽略:watch 已处理 UI 反馈
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听安装进度终态,弹 toast 并延时清空进度
|
||||||
|
watch(
|
||||||
|
() => store.installProgress?.stage,
|
||||||
|
(stage) => {
|
||||||
|
if (stage === 'done') {
|
||||||
|
toast.success('内核安装完成', {
|
||||||
|
description: store.installProgress?.message
|
||||||
|
})
|
||||||
|
// 2 秒后清空进度,让用户看到 100% 终态
|
||||||
|
setTimeout(() => store.clearInstallProgress(), 2000)
|
||||||
|
} else if (stage === 'error') {
|
||||||
|
toast.error('内核安装失败', {
|
||||||
|
description: store.installProgress?.message
|
||||||
|
})
|
||||||
|
setTimeout(() => store.clearInstallProgress(), 5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// ===== 节点 =====
|
// ===== 节点 =====
|
||||||
const refreshProxies = async () => {
|
const refreshProxies = async () => {
|
||||||
await loadProxiesWithError()
|
await loadProxiesWithError()
|
||||||
@@ -761,7 +851,8 @@ const saveSettingsForm = async () => {
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<!-- 概览 -->
|
<!-- 概览 -->
|
||||||
<TabsContent value="overview" class="flex-1 mt-4 overflow-y-auto tab-animate">
|
<TabsContent value="overview" class="flex-1 mt-4 tab-animate">
|
||||||
|
<ScrollArea class="h-full pr-3">
|
||||||
<div class="columns-1 md:columns-2 gap-4 [&>*]:mb-4 [&>*]:break-inside-avoid">
|
<div class="columns-1 md:columns-2 gap-4 [&>*]:mb-4 [&>*]:break-inside-avoid">
|
||||||
<!-- 内核状态 -->
|
<!-- 内核状态 -->
|
||||||
<Card>
|
<Card>
|
||||||
@@ -775,7 +866,7 @@ const saveSettingsForm = async () => {
|
|||||||
@click="handleCheckUpdate"
|
@click="handleCheckUpdate"
|
||||||
>
|
>
|
||||||
<Loader2 v-if="checkingUpdate" class="size-3 animate-spin" />
|
<Loader2 v-if="checkingUpdate" class="size-3 animate-spin" />
|
||||||
<Download v-else class="size-3" />检查更新
|
<Download v-else key="icon-download" class="size-3" />检查更新
|
||||||
</Button>
|
</Button>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -789,7 +880,7 @@ const saveSettingsForm = async () => {
|
|||||||
<AlertCircle class="size-3.5" />未安装
|
<AlertCircle class="size-3.5" />未安装
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div v-if="store.kernel?.exists" class="flex items-center justify-between">
|
||||||
<span class="text-muted-foreground">当前版本</span>
|
<span class="text-muted-foreground">当前版本</span>
|
||||||
<span class="font-mono text-xs" :title="store.kernel?.version ?? ''">{{ versionShort }}</span>
|
<span class="font-mono text-xs" :title="store.kernel?.version ?? ''">{{ versionShort }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -799,14 +890,15 @@ const saveSettingsForm = async () => {
|
|||||||
{{ kernelUpdateInfo.latestVersion }}
|
{{ kernelUpdateInfo.latestVersion }}
|
||||||
<Button
|
<Button
|
||||||
v-if="kernelUpdateInfo.hasUpdate"
|
v-if="kernelUpdateInfo.hasUpdate"
|
||||||
|
key="btn-update"
|
||||||
size="xs" variant="default"
|
size="xs" variant="default"
|
||||||
:disabled="updatingKernel"
|
:disabled="updatingKernel"
|
||||||
@click="handleUpdateKernel"
|
@click="handleUpdateKernel"
|
||||||
>
|
>
|
||||||
<Loader2 v-if="updatingKernel" class="size-3 animate-spin" />
|
<Loader2 v-if="updatingKernel" class="size-3 animate-spin" />
|
||||||
<Download v-else class="size-3" />更新
|
<Download v-else key="icon-download" class="size-3" />更新
|
||||||
</Button>
|
</Button>
|
||||||
<Check v-else class="size-3 text-emerald-500" />
|
<Check v-else key="icon-updated" class="size-3 text-emerald-500" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
@@ -828,9 +920,96 @@ const saveSettingsForm = async () => {
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="!store.kernel?.exists" class="text-xs text-amber-600 dark:text-amber-500 leading-relaxed">
|
|
||||||
请将 mihomo.exe 放到 <code class="px-1 bg-muted rounded">src-tauri/binaries/</code> 后重启应用,或直接放到上述 cores 目录。
|
<!-- 首次安装区块:仅在内核未安装且不在安装中时显示 -->
|
||||||
</p>
|
<div
|
||||||
|
v-if="!store.kernel?.exists && !store.installProgress"
|
||||||
|
class="space-y-2 pt-2 border-t"
|
||||||
|
>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<Label class="text-xs text-muted-foreground">下载源</Label>
|
||||||
|
<Select v-model="mirrorChoice">
|
||||||
|
<SelectTrigger size="sm" class="w-full">
|
||||||
|
<SelectValue placeholder="选择下载源" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem
|
||||||
|
v-for="m in MIRROR_PRESETS"
|
||||||
|
:key="m.value"
|
||||||
|
:value="m.value"
|
||||||
|
>
|
||||||
|
{{ m.label }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
{{ MIRROR_PRESETS.find(m => m.value === mirrorChoice)?.hint }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="mirrorChoice === '__custom'" class="space-y-1.5">
|
||||||
|
<Label class="text-xs text-muted-foreground">镜像站前缀</Label>
|
||||||
|
<Input
|
||||||
|
v-model="customMirror"
|
||||||
|
placeholder="如 https://ghproxy.net/"
|
||||||
|
class="h-8 text-xs"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
前缀会拼接到 GitHub 下载链接前,需以 / 结尾(自动补全)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm" variant="default" class="w-full"
|
||||||
|
:disabled="store.installing || (mirrorChoice === '__custom' && !customMirror.trim())"
|
||||||
|
@click="handleInstallKernel"
|
||||||
|
>
|
||||||
|
<DownloadCloud class="size-4" />安装内核
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 安装进度区块 -->
|
||||||
|
<div
|
||||||
|
v-if="store.installProgress"
|
||||||
|
class="rounded-md border p-3 space-y-2 bg-muted/30"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between text-xs">
|
||||||
|
<span :class="installStageColor" class="flex items-center gap-1.5 font-medium">
|
||||||
|
<Loader2
|
||||||
|
v-if="['downloading', 'extracting', 'replacing'].includes(store.installProgress.stage)"
|
||||||
|
key="stage-loading"
|
||||||
|
class="size-3 animate-spin"
|
||||||
|
/>
|
||||||
|
<Check v-else-if="store.installProgress.stage === 'done'" key="stage-done" class="size-3" />
|
||||||
|
<AlertCircle v-else-if="store.installProgress.stage === 'error'" key="stage-error" class="size-3" />
|
||||||
|
{{ installStageText }}
|
||||||
|
</span>
|
||||||
|
<span v-if="installHasTotal && store.installProgress.stage === 'downloading'" class="font-mono text-muted-foreground">
|
||||||
|
{{ store.installProgress.percent }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Progress
|
||||||
|
v-if="installHasTotal || store.installProgress.stage !== 'downloading'"
|
||||||
|
key="progress-bar"
|
||||||
|
:model-value="installPercentDisplay"
|
||||||
|
class="h-2"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
key="progress-indeterminate"
|
||||||
|
class="h-2 w-full overflow-hidden rounded-full bg-primary/20 relative"
|
||||||
|
>
|
||||||
|
<div class="absolute inset-y-0 left-0 w-1/3 bg-primary rounded-full animate-[indeterminate_1.2s_ease-in-out_infinite]" />
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
<template v-if="store.installProgress.stage === 'downloading'">
|
||||||
|
{{ store.installProgress.message }}
|
||||||
|
<span v-if="installHasTotal" class="ml-1">
|
||||||
|
({{ formatMB(store.installProgress.downloadedBytes) }} / {{ formatMB(store.installProgress.totalBytes!) }})
|
||||||
|
</span>
|
||||||
|
<span v-else class="ml-1">{{ formatMB(store.installProgress.downloadedBytes) }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>{{ store.installProgress.message }}</template>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -865,18 +1044,18 @@ const saveSettingsForm = async () => {
|
|||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Button v-if="!running" size="sm" :disabled="starting" @click="handleStart">
|
<Button v-if="!running" key="btn-start" size="sm" :disabled="starting" @click="handleStart">
|
||||||
<Loader2 v-if="starting" class="size-3.5 animate-spin" />
|
<Loader2 v-if="starting" key="starting-loading" class="size-3.5 animate-spin" />
|
||||||
<Play v-else class="size-3.5" />启动
|
<Play v-else key="starting-icon" class="size-3.5" />启动
|
||||||
</Button>
|
</Button>
|
||||||
<template v-else>
|
<template v-else key="btn-stop-group">
|
||||||
<Button size="sm" variant="destructive" :disabled="stopping" @click="handleStop">
|
<Button size="sm" variant="destructive" :disabled="stopping" @click="handleStop">
|
||||||
<Loader2 v-if="stopping" class="size-3.5 animate-spin" />
|
<Loader2 v-if="stopping" key="stopping-loading" class="size-3.5 animate-spin" />
|
||||||
<Square v-else class="size-3.5" />停止
|
<Square v-else key="stopping-icon" class="size-3.5" />停止
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="outline" :disabled="restarting" @click="handleRestart">
|
<Button size="sm" variant="outline" :disabled="restarting" @click="handleRestart">
|
||||||
<Loader2 v-if="restarting" class="size-3.5 animate-spin" />
|
<Loader2 v-if="restarting" key="restarting-loading" class="size-3.5 animate-spin" />
|
||||||
<RotateCw v-else class="size-3.5" />重启
|
<RotateCw v-else key="restarting-icon" class="size-3.5" />重启
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -919,7 +1098,7 @@ const saveSettingsForm = async () => {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="space-y-3 text-sm">
|
<CardContent class="space-y-3 text-sm">
|
||||||
<template v-if="mainGroupName">
|
<template v-if="mainGroupName" key="has-main">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-muted-foreground">代理组</span>
|
<span class="text-muted-foreground">代理组</span>
|
||||||
<span class="font-medium">{{ mainGroupName }}</span>
|
<span class="font-medium">{{ mainGroupName }}</span>
|
||||||
@@ -947,7 +1126,7 @@ const saveSettingsForm = async () => {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="text-center text-muted-foreground py-4 text-xs">
|
<div v-else key="no-main" class="text-center text-muted-foreground py-4 text-xs">
|
||||||
{{ running ? '暂无可选节点,请先导入订阅' : 'mihomo 未运行' }}
|
{{ running ? '暂无可选节点,请先导入订阅' : 'mihomo 未运行' }}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -1037,28 +1216,30 @@ const saveSettingsForm = async () => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<!-- 节点 -->
|
<!-- 节点 -->
|
||||||
<TabsContent value="proxies" class="flex-1 mt-4 min-h-0 tab-animate">
|
<TabsContent value="proxies" class="flex-1 mt-4 tab-animate">
|
||||||
<div v-if="!running" class="h-full flex flex-col items-center justify-center text-muted-foreground gap-2">
|
<div v-if="!running" key="not-running" class="h-full flex flex-col items-center justify-center text-muted-foreground gap-2 pt-10 pr-10">
|
||||||
<Server class="size-12 opacity-30" />
|
<Server class="size-12 opacity-30" />
|
||||||
<p class="text-sm">mihomo 未运行,请先在概览页启动</p>
|
<p class="text-sm">mihomo 未运行,请先在概览页启动</p>
|
||||||
</div>
|
</div>
|
||||||
<ScrollArea v-else class="h-full pr-3">
|
<ScrollArea v-else key="proxy-list" class="h-full pr-3">
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-3">
|
||||||
<p v-if="!groups.length" class="text-sm text-muted-foreground">暂无代理组</p>
|
<p v-if="!groups.length" class="text-sm text-muted-foreground">暂无代理组</p>
|
||||||
<p v-else class="text-sm text-muted-foreground">{{ groups.length }} 个代理组</p>
|
<p v-else class="text-sm text-muted-foreground">{{ groups.length }} 个代理组</p>
|
||||||
<Button size="xs" variant="outline" :disabled="loadingProxies" @click="refreshProxies">
|
<Button size="xs" variant="outline" :disabled="loadingProxies" @click="refreshProxies">
|
||||||
<Loader2 v-if="loadingProxies" class="size-3 animate-spin" />
|
<Loader2 v-if="loadingProxies" key="loading-proxies" class="size-3 animate-spin" />
|
||||||
<RefreshCw v-else class="size-3" />刷新
|
<RefreshCw v-else key="refresh-icon" class="size-3" />刷新
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!groups.length" class="text-center text-sm text-muted-foreground py-8">
|
<div v-if="!groups.length" key="no-groups" class="text-center text-sm text-muted-foreground py-8">
|
||||||
未能加载代理组,请点击刷新重试
|
未能加载代理组,请点击刷新重试
|
||||||
</div>
|
</div>
|
||||||
<Accordion
|
<Accordion
|
||||||
v-else
|
v-else
|
||||||
|
key="groups-list"
|
||||||
v-model="accordionValue"
|
v-model="accordionValue"
|
||||||
type="single"
|
type="single"
|
||||||
collapsible
|
collapsible
|
||||||
@@ -1081,8 +1262,8 @@ const saveSettingsForm = async () => {
|
|||||||
:disabled="testingGroups.has(gname)"
|
:disabled="testingGroups.has(gname)"
|
||||||
@click.stop="testGroup(gname)"
|
@click.stop="testGroup(gname)"
|
||||||
>
|
>
|
||||||
<Loader2 v-if="testingGroups.has(gname)" class="size-3 animate-spin" />
|
<Loader2 v-if="testingGroups.has(gname)" key="testing" class="size-3 animate-spin" />
|
||||||
<Zap v-else class="size-3" />测速
|
<Zap v-else key="test-icon" class="size-3" />测速
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1111,7 +1292,8 @@ const saveSettingsForm = async () => {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<!-- 订阅 -->
|
<!-- 订阅 -->
|
||||||
<TabsContent value="profiles" class="flex-1 mt-4 overflow-y-auto tab-animate">
|
<TabsContent value="profiles" class="flex-1 mt-4 tab-animate">
|
||||||
|
<ScrollArea class="h-full pr-3">
|
||||||
<div class="space-y-4 max-w-3xl">
|
<div class="space-y-4 max-w-3xl">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader class="pb-3">
|
<CardHeader class="pb-3">
|
||||||
@@ -1129,8 +1311,8 @@ const saveSettingsForm = async () => {
|
|||||||
<Input id="sub-name" v-model="importName" placeholder="我的订阅" />
|
<Input id="sub-name" v-model="importName" placeholder="我的订阅" />
|
||||||
</div>
|
</div>
|
||||||
<Button size="sm" :disabled="importing" @click="doImport">
|
<Button size="sm" :disabled="importing" @click="doImport">
|
||||||
<Loader2 v-if="importing" class="size-3.5 animate-spin" />
|
<Loader2 v-if="importing" key="importing" class="size-3.5 animate-spin" />
|
||||||
<Upload v-else class="size-3.5" />导入
|
<Upload v-else key="upload-icon" class="size-3.5" />导入
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -1179,10 +1361,12 @@ const saveSettingsForm = async () => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<!-- 设置 -->
|
<!-- 设置 -->
|
||||||
<TabsContent value="settings" class="flex-1 mt-4 overflow-y-auto tab-animate">
|
<TabsContent value="settings" class="flex-1 mt-4 tab-animate">
|
||||||
|
<ScrollArea class="h-full pr-3">
|
||||||
<Card class="max-w-2xl">
|
<Card class="max-w-2xl">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="flex items-center gap-2 text-base">
|
<CardTitle class="flex items-center gap-2 text-base">
|
||||||
@@ -1193,8 +1377,8 @@ const saveSettingsForm = async () => {
|
|||||||
<!-- 保存设置(顶部醒目位置) -->
|
<!-- 保存设置(顶部醒目位置) -->
|
||||||
<div class="flex items-center gap-3 rounded-md border border-primary/30 bg-primary/5 p-3">
|
<div class="flex items-center gap-3 rounded-md border border-primary/30 bg-primary/5 p-3">
|
||||||
<Button size="sm" :disabled="savingSettings" @click="saveSettingsForm">
|
<Button size="sm" :disabled="savingSettings" @click="saveSettingsForm">
|
||||||
<Loader2 v-if="savingSettings" class="size-3.5 animate-spin" />
|
<Loader2 v-if="savingSettings" key="saving" class="size-3.5 animate-spin" />
|
||||||
<Check v-else class="size-3.5" />保存设置
|
<Check v-else key="saved-icon" class="size-3.5" />保存设置
|
||||||
</Button>
|
</Button>
|
||||||
<p class="text-xs text-muted-foreground flex-1">
|
<p class="text-xs text-muted-foreground flex-1">
|
||||||
修改端口/接口/密钥/模式后需重启 mihomo 生效。DNS、规则等高级配置请直接编辑订阅文件。
|
修改端口/接口/密钥/模式后需重启 mihomo 生效。DNS、规则等高级配置请直接编辑订阅文件。
|
||||||
@@ -1269,6 +1453,7 @@ const saveSettingsForm = async () => {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</ScrollArea>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
|
||||||
import { createLogger } from '@/lib/logger'
|
import { createLogger } from '@/lib/logger'
|
||||||
|
|
||||||
const logger = createLogger('proxy')
|
const logger = createLogger('proxy')
|
||||||
@@ -23,6 +24,18 @@ export interface ProxySettings {
|
|||||||
autoSwitchInterval: number
|
autoSwitchInterval: number
|
||||||
autoSwitchGroup: string
|
autoSwitchGroup: string
|
||||||
autoSwitchRegion: string
|
autoSwitchRegion: string
|
||||||
|
/** 内核下载镜像源前缀列表(空串=直连 GitHub) */
|
||||||
|
kernelMirrors: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 内核安装进度事件载荷,对应 Rust 端 InstallProgress */
|
||||||
|
export interface InstallProgress {
|
||||||
|
/** downloading | extracting | replacing | done | error */
|
||||||
|
stage: string
|
||||||
|
percent: number
|
||||||
|
downloadedBytes: number
|
||||||
|
totalBytes: number | null
|
||||||
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProfileMeta {
|
export interface ProfileMeta {
|
||||||
@@ -85,6 +98,11 @@ export const useProxyStore = defineStore('proxy', () => {
|
|||||||
const settings = ref<ProxySettings | null>(null)
|
const settings = ref<ProxySettings | null>(null)
|
||||||
const systemProxy = ref(false)
|
const systemProxy = ref(false)
|
||||||
|
|
||||||
|
// ===== 内核安装进度 =====
|
||||||
|
const installing = ref(false)
|
||||||
|
const installProgress = ref<InstallProgress | null>(null)
|
||||||
|
let progressUnlisten: UnlistenFn | null = null
|
||||||
|
|
||||||
/** 内核信息(同时尝试从 resource 提取到 cores/) */
|
/** 内核信息(同时尝试从 resource 提取到 cores/) */
|
||||||
const refreshKernel = async () => {
|
const refreshKernel = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -246,16 +264,59 @@ export const useProxyStore = defineStore('proxy', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 内核更新 ----------
|
// ---------- 内核更新 / 安装 ----------
|
||||||
const checkKernelUpdate = async (): Promise<KernelUpdateInfo> => {
|
const checkKernelUpdate = async (): Promise<KernelUpdateInfo> => {
|
||||||
return await invoke<KernelUpdateInfo>('proxy_check_kernel_update')
|
return await invoke<KernelUpdateInfo>('proxy_check_kernel_update')
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateKernel = async () => {
|
const updateKernel = async (mirrorPrefix: string = '') => {
|
||||||
await invoke('proxy_update_kernel')
|
await invoke('proxy_update_kernel', { mirrorPrefix })
|
||||||
await refreshKernel()
|
await refreshKernel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首次安装内核:调用后端 install_kernel,监听 kernel-install-progress 事件更新进度
|
||||||
|
* @param mirrorPrefix 镜像源前缀(空串=GitHub 直连)
|
||||||
|
* 完成或出错后自动取消监听并清空进度(由调用方控制何时隐藏 UI)
|
||||||
|
*/
|
||||||
|
const installKernel = async (mirrorPrefix: string = ''): Promise<void> => {
|
||||||
|
if (installing.value) return
|
||||||
|
installing.value = true
|
||||||
|
installProgress.value = {
|
||||||
|
stage: 'downloading',
|
||||||
|
percent: 0,
|
||||||
|
downloadedBytes: 0,
|
||||||
|
totalBytes: null,
|
||||||
|
message: '准备开始下载...'
|
||||||
|
}
|
||||||
|
// 注册进度事件监听
|
||||||
|
if (!progressUnlisten) {
|
||||||
|
progressUnlisten = await listen<InstallProgress>('kernel-install-progress', (e) => {
|
||||||
|
installProgress.value = e.payload
|
||||||
|
})
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await invoke('proxy_install_kernel', { mirrorPrefix })
|
||||||
|
await refreshKernel()
|
||||||
|
} catch (e) {
|
||||||
|
// 错误事件已由后端 emit,这里仅记录日志
|
||||||
|
logger.error('内核安装失败: ' + e)
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
// 保留 installProgress 一段时间供 UI 显示终态,由调用方负责清空
|
||||||
|
installing.value = false
|
||||||
|
if (progressUnlisten) {
|
||||||
|
progressUnlisten()
|
||||||
|
progressUnlisten = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清空进度状态(UI 在动画结束后调用) */
|
||||||
|
const clearInstallProgress = () => {
|
||||||
|
installProgress.value = null
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// state
|
// state
|
||||||
kernel,
|
kernel,
|
||||||
@@ -264,6 +325,8 @@ export const useProxyStore = defineStore('proxy', () => {
|
|||||||
proxies,
|
proxies,
|
||||||
settings,
|
settings,
|
||||||
systemProxy,
|
systemProxy,
|
||||||
|
installing,
|
||||||
|
installProgress,
|
||||||
// kernel & process
|
// kernel & process
|
||||||
refreshKernel,
|
refreshKernel,
|
||||||
refreshStatus,
|
refreshStatus,
|
||||||
@@ -289,8 +352,10 @@ export const useProxyStore = defineStore('proxy', () => {
|
|||||||
setSystemProxy,
|
setSystemProxy,
|
||||||
clearSystemProxy,
|
clearSystemProxy,
|
||||||
toggleSystemProxy,
|
toggleSystemProxy,
|
||||||
// kernel update
|
// kernel update / install
|
||||||
checkKernelUpdate,
|
checkKernelUpdate,
|
||||||
updateKernel
|
updateKernel,
|
||||||
|
installKernel,
|
||||||
|
clearInstallProgress
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -173,3 +173,13 @@
|
|||||||
[data-sonner-toaster] {
|
[data-sonner-toaster] {
|
||||||
z-index: 99999 !important;
|
z-index: 99999 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 进度条 indeterminate 动画(无 Content-Length 时使用) */
|
||||||
|
@keyframes indeterminate {
|
||||||
|
0% {
|
||||||
|
left: -33%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 778 KiB |