下载/主界面优化
This commit is contained in:
+35
-24
@@ -19,7 +19,7 @@
|
|||||||
src/modules/<module-name>/
|
src/modules/<module-name>/
|
||||||
├── index.ts # 模块配置(ModuleConfig)
|
├── index.ts # 模块配置(ModuleConfig)
|
||||||
├── ModuleComponent.vue # 前端组件
|
├── ModuleComponent.vue # 前端组件
|
||||||
src-tauri/src/commands/ # Rust 命令(可选)
|
src-tauri/src/ # Rust 后端逻辑(按功能分文件/模块目录)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 模块配置类型
|
#### 模块配置类型
|
||||||
@@ -167,10 +167,10 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
### Rust 命令模板
|
### Rust 命令模板
|
||||||
|
|
||||||
创建 `src-tauri/src/commands/<name>.rs`:
|
在 `src-tauri/src/` 中创建模块文件(如 `src-tauri/src/my_module.rs` 或 `src-tauri/src/my_module/` 目录),并在 `lib.rs` 中注册:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// commands/module_name.rs
|
// src-tauri/src/my_module.rs
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
|
||||||
@@ -230,9 +230,9 @@ fn save_data(data: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
```rust
|
```rust
|
||||||
// lib.rs
|
// lib.rs
|
||||||
mod commands;
|
mod my_module;
|
||||||
|
|
||||||
use commands::module_name::{module_get_data, module_set_data};
|
use my_module::{module_get_data, module_set_data};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn greet(name: &str) -> String {
|
fn greet(name: &str) -> String {
|
||||||
@@ -254,18 +254,19 @@ pub fn run() {
|
|||||||
|
|
||||||
## 常用 Tauri 插件
|
## 常用 Tauri 插件
|
||||||
|
|
||||||
| 插件 | 用途 | 安装命令 |
|
项目当前使用的 Tauri 插件(见 `Cargo.toml`):
|
||||||
|------|------|----------|
|
|
||||||
| `tauri-plugin-autostart` | 开机自启 | `cargo add tauri-plugin-autostart` |
|
| 插件 | 用途 |
|
||||||
| `tauri-plugin-tray` | 系统托盘 | `cargo add tauri-plugin-tray` |
|
|------|------|
|
||||||
| `tauri-plugin-log` | 日志系统 | `cargo add tauri-plugin-log` |
|
| `tauri-plugin-autostart` | 开机自启 |
|
||||||
| `tauri-plugin-shell` | 执行命令 | `cargo add tauri-plugin-shell` |
|
| `tauri-plugin-opener` | 打开文件/目录/URL(绕过 IPC scope 限制) |
|
||||||
| `tauri-plugin-clipboard-manager` | 剪贴板 | `cargo add tauri-plugin-clipboard-manager` |
|
| `tauri-plugin-dialog` | 文件/目录选择对话框 |
|
||||||
| `tauri-plugin-window-state` | 窗口状态 | `cargo add tauri-plugin-window-state` |
|
|
||||||
|
> 系统托盘(tray)和窗口效果(mica/acrylic)是 Tauri 2 内置能力,无需额外插件。日志系统为自建(`src-tauri/src/logger.rs`),未使用 `tauri-plugin-log`。
|
||||||
|
|
||||||
## 进程管理
|
## 进程管理
|
||||||
|
|
||||||
对于需要管理外部进程的模块(如 mihomo、aria2),使用内置的 `ProcessManager`:
|
对于需要管理外部子进程的模块(如 mihomo),使用内置的 `ProcessManager`。下载器模块使用进程内自建下载引擎(`src-tauri/src/download_engine/`),不涉及外部子进程:
|
||||||
|
|
||||||
### 架构
|
### 架构
|
||||||
|
|
||||||
@@ -349,21 +350,31 @@ rusqlite = { version = "0.30", features = ["bundled"] }
|
|||||||
|
|
||||||
## 权限配置
|
## 权限配置
|
||||||
|
|
||||||
在 `src-tauri/capabilities/default.json` 中配置权限:
|
在 `src-tauri/capabilities/default.json` 中配置权限(实际项目配置):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/schemas/desktop-capability.json",
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
"identifier": "default",
|
"identifier": "default",
|
||||||
"description": "Default capabilities for the app",
|
"description": "Capability for the main window",
|
||||||
"windows": ["main"],
|
"windows": ["main"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"shell:allow-execute",
|
"opener:default",
|
||||||
"shell:allow-spawn",
|
"opener:allow-reveal-item-in-dir",
|
||||||
"path:allow-app-data-dir",
|
"opener:allow-open-path",
|
||||||
"path:allow-read",
|
"dialog:default",
|
||||||
"path:allow-write"
|
"core:window:allow-minimize",
|
||||||
|
"core:window:allow-maximize",
|
||||||
|
"core:window:allow-close",
|
||||||
|
"core:window:allow-toggle-maximize",
|
||||||
|
"core:window:allow-hide",
|
||||||
|
"core:window:allow-show",
|
||||||
|
"core:window:allow-set-focus",
|
||||||
|
"core:window:allow-start-dragging",
|
||||||
|
"core:window:allow-set-effects",
|
||||||
|
"core:window:allow-set-background-color",
|
||||||
|
"core:window:allow-set-theme"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -425,8 +436,8 @@ let path = Path::new("data").join("config.json");
|
|||||||
# 创建前端模块目录
|
# 创建前端模块目录
|
||||||
mkdir -p src/modules/new-module
|
mkdir -p src/modules/new-module
|
||||||
|
|
||||||
# 创建 Rust 命令文件
|
# 创建 Rust 模块文件
|
||||||
touch src-tauri/src/commands/new_module.rs
|
touch src-tauri/src/new_module.rs
|
||||||
```
|
```
|
||||||
|
|
||||||
### 模板文件
|
### 模板文件
|
||||||
|
|||||||
+11
-12
@@ -142,7 +142,7 @@ export const moduleIconMap: Record<string, Component> = {
|
|||||||
|
|
||||||
## 进程管理
|
## 进程管理
|
||||||
|
|
||||||
需要管理外部子进程的模块(如 mihomo、aria2)通过 `ModuleProcessConfig` 声明进程配置。
|
需要管理外部子进程的模块(如 mihomo)通过 `ModuleProcessConfig` 声明进程配置。下载器模块使用进程内自建下载引擎,不涉及外部子进程管理。
|
||||||
|
|
||||||
### 配置示例
|
### 配置示例
|
||||||
|
|
||||||
@@ -370,8 +370,7 @@ appStore.toggleModule(moduleId, enabled)
|
|||||||
|
|
||||||
- `ProcessManager::start()` 启动 mihomo 等内核
|
- `ProcessManager::start()` 启动 mihomo 等内核
|
||||||
- `ProcessManager::check_and_cleanup()` 崩溃重启
|
- `ProcessManager::check_and_cleanup()` 崩溃重启
|
||||||
- 任何调用 `mihomo.exe -v`、`aria2c --version` 等查询版本的场景
|
- 任何调用 `mihomo.exe -v` 等查询版本的场景
|
||||||
- 未来下载器模块启动 aria2 时同样适用
|
|
||||||
|
|
||||||
跨平台:非 Windows 平台该函数为空实现,无需条件编译。
|
跨平台:非 Windows 平台该函数为空实现,无需条件编译。
|
||||||
|
|
||||||
@@ -398,7 +397,7 @@ thread::spawn(move || {
|
|||||||
|
|
||||||
### 后端:内核/二进制下载用流式 + 事件推送
|
### 后端:内核/二进制下载用流式 + 事件推送
|
||||||
|
|
||||||
`mihomo_manager.rs` 的 `install_kernel` 确立了"下载二进制资源"的标准模式,未来下载器模块下载 aria2 内核时应复用:
|
`mihomo_manager.rs` 的 `install_kernel` 确立了"下载二进制资源"的标准模式,未来若有其他模块需要下载外部内核时应复用:
|
||||||
|
|
||||||
- **流式下载**:`reqwest::Response::bytes_stream()` + `futures_util::StreamExt`,避免大文件一次性读入内存
|
- **流式下载**:`reqwest::Response::bytes_stream()` + `futures_util::StreamExt`,避免大文件一次性读入内存
|
||||||
- **进度事件**:通过 `app.emit("xxx-install-progress", progress)` 推送,事件载荷结构参考 `InstallProgress`
|
- **进度事件**:通过 `app.emit("xxx-install-progress", progress)` 推送,事件载荷结构参考 `InstallProgress`
|
||||||
@@ -550,21 +549,21 @@ const pathDisplay = computed(() => {
|
|||||||
|
|
||||||
`<title>` 放完整路径,显示文本用 `pathDisplay`,复制时复制完整路径。
|
`<title>` 放完整路径,显示文本用 `pathDisplay`,复制时复制完整路径。
|
||||||
|
|
||||||
### 前端:任务历史 localStorage 持久化(下载器模块模式)
|
### 前端:任务历史持久化(下载器模块模式)
|
||||||
|
|
||||||
子进程未启动时仍想展示历史数据(如下载任务列表),可在 store 中用 localStorage 保存最近一次的任务快照:
|
下载引擎在进程内运行,状态通过 `engine_state.json` 持久化到磁盘。前端 store 的做法:
|
||||||
|
|
||||||
- 每次全量刷新后调用 `persistHistory()` 保存(合并去重,按状态优先级排序,限制条数如 200)
|
- `onMounted` 时调用 `store.init()`,一次性加载 status/settings/extensionInfo/tasks
|
||||||
- `onMounted` 时无论子进程是否运行都先调用 `loadHistory()` 填充到 `stoppedTasks`
|
- 通过 Tauri 事件 `download-progress`、`download-complete`、`download-added` 实时更新任务状态
|
||||||
- 子进程启动后实时数据会覆盖历史快照
|
- 引擎在 `Storage::new` 时自动创建数据目录,`save` 时兜底重建父目录,避免路径不存在错误
|
||||||
|
|
||||||
此模式适用于任何"子进程不运行时也要展示历史"的场景。
|
此模式适用于任何"引擎在进程内运行、状态需持久化"的场景。
|
||||||
|
|
||||||
### 前端:轻量设置 Dialog(替代独立 Tab)
|
### 前端:轻量设置 Dialog(替代独立 Tab)
|
||||||
|
|
||||||
模块设置项较多时,传统做法是单独开一个"设置"Tab。但任务页工具栏需要快速调整少量核心设置(如下载目录、并发数),切到设置 Tab 再切回来体验割裂。
|
模块设置项较多时,传统做法是单独开一个"设置"Tab。但任务页工具栏需要快速调整少量核心设置(如下载目录、并发数),切到设置 Tab 再切回来体验割裂。
|
||||||
|
|
||||||
**模式**:在任务工具栏放一个"下载设置"按钮,点击弹出 Dialog,包含核心设置项(与设置页共用 `store.settings`),保存时调用 `handleSaveSettings`(重启子进程应用配置)并自动关闭弹窗。完整设置仍保留在"设置"Tab。
|
**模式**:在任务工具栏放一个"下载设置"按钮,点击弹出 Dialog,包含核心设置项(与设置页共用 `store.settings`),保存时调用 `handleSaveSettings`(引擎立即应用新配置,无需重启)并自动关闭弹窗。完整设置仍保留在"设置"Tab。
|
||||||
|
|
||||||
适用场景:需要在任务页快速调整的少量高频设置;若设置项不多,可完全用 Dialog 替代设置 Tab。
|
适用场景:需要在任务页快速调整的少量高频设置;若设置项不多,可完全用 Dialog 替代设置 Tab。
|
||||||
|
|
||||||
@@ -621,4 +620,4 @@ const formatEta = (seconds: number): string => {
|
|||||||
1. `quit_app` Tauri 命令(前端 `appWindow.destroy()` 触发)
|
1. `quit_app` Tauri 命令(前端 `appWindow.destroy()` 触发)
|
||||||
2. 系统托盘的退出菜单项
|
2. 系统托盘的退出菜单项
|
||||||
|
|
||||||
漏掉任何一个都会导致子进程残留(如 aria2 继续占用端口、mihomo 系统代理未清除)。新增管理子进程的模块时,检查这两处是否都调用了清理逻辑。
|
漏掉任何一个都会导致子进程残留(如 mihomo 继续占用端口、系统代理未清除)。新增管理子进程的模块时,检查这两处是否都调用了清理逻辑。
|
||||||
|
|||||||
@@ -19,41 +19,39 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
Thing/
|
Thing/
|
||||||
├── public/ # 静态资源
|
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── assets/ # 前端资源
|
|
||||||
│ ├── components/ # 通用组件
|
│ ├── components/ # 通用组件
|
||||||
│ │ ├── layout/ # 布局组件
|
│ │ ├── layout/ # 布局组件(TitleBar、Sidebar、ModuleContainer)
|
||||||
│ │ └── ui/ # shadcn-vue UI 组件
|
│ │ └── ui/ # shadcn-vue UI 组件
|
||||||
│ ├── composables/ # Vue 组合式函数
|
│ ├── lib/ # 工具库(logger、useModuleTabs、utils)
|
||||||
│ ├── lib/ # 工具库(cn 等)
|
│ ├── modules/ # 功能模块(每个模块含 index.ts 配置 + Vue 组件)
|
||||||
│ ├── modules/ # 功能模块
|
|
||||||
│ │ ├── proxy/ # 代理管理模块
|
│ │ ├── proxy/ # 代理管理模块
|
||||||
│ │ ├── clipboard/ # 剪贴板增强模块
|
│ │ ├── clipboard/ # 剪贴板增强模块
|
||||||
│ │ ├── screenshot/ # 截图模块
|
│ │ ├── screenshot/ # 截图模块
|
||||||
│ │ ├── monitor/ # 硬件监控模块
|
│ │ ├── monitor/ # 硬件监控模块
|
||||||
│ │ ├── downloader/ # 下载器模块
|
│ │ ├── downloader/ # 下载器模块
|
||||||
│ │ └── finder/ # 文件搜索模块
|
│ │ ├── finder/ # 文件搜索模块
|
||||||
|
│ │ ├── general/ # 通用设置模块
|
||||||
|
│ │ ├── icons.ts # 模块图标映射
|
||||||
|
│ │ ├── index.ts # 模块聚合入口
|
||||||
|
│ │ └── registry.ts # 模块注册表单例
|
||||||
│ ├── stores/ # Pinia 状态管理
|
│ ├── stores/ # Pinia 状态管理
|
||||||
│ ├── types/ # TypeScript 类型定义
|
│ ├── types/ # TypeScript 类型定义
|
||||||
│ ├── App.vue # 主应用组件
|
│ ├── App.vue # 主应用组件
|
||||||
│ ├── main.ts # 入口文件
|
│ ├── main.ts # 入口文件
|
||||||
│ └── assets/main.css # 全局样式
|
│ └── style.css # 全局样式
|
||||||
├── src-tauri/
|
├── src-tauri/
|
||||||
|
│ ├── binaries/ # 外部内核(mihomo.exe)
|
||||||
│ ├── capabilities/ # Tauri 权限配置
|
│ ├── capabilities/ # Tauri 权限配置
|
||||||
│ ├── icons/ # 应用图标
|
│ ├── icons/ # 应用图标
|
||||||
|
│ ├── resources/ # 打包资源(浏览器扩展等)
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
│ │ ├── commands/ # Rust 命令
|
│ │ ├── download_engine/ # 下载引擎(进程内自建,含 engine/storage/http_dl/server 等)
|
||||||
│ │ │ ├── proxy.rs # 代理相关命令
|
│ │ ├── logger.rs # 日志系统
|
||||||
│ │ │ ├── clipboard.rs # 剪贴板相关命令
|
│ │ ├── mihomo_manager.rs # mihomo 内核管理(代理模块后端)
|
||||||
│ │ │ ├── screenshot.rs # 截图相关命令
|
│ │ ├── process_manager.rs # 子进程统一管理
|
||||||
│ │ │ ├── monitor.rs # 监控相关命令
|
|
||||||
│ │ │ ├── downloader.rs # 下载器相关命令
|
|
||||||
│ │ │ └── finder.rs # 文件搜索相关命令
|
|
||||||
│ │ ├── modules/ # Rust 模块逻辑
|
|
||||||
│ │ ├── utils/ # Rust 工具函数
|
|
||||||
│ │ ├── main.rs # Rust 入口
|
│ │ ├── main.rs # Rust 入口
|
||||||
│ │ └── lib.rs # Rust 库入口
|
│ │ └── lib.rs # Rust 库入口(命令注册、应用初始化)
|
||||||
│ ├── Cargo.toml # Rust 依赖配置
|
│ ├── Cargo.toml # Rust 依赖配置
|
||||||
│ └── tauri.conf.json # Tauri 配置
|
│ └── tauri.conf.json # Tauri 配置
|
||||||
├── package.json # 前端依赖配置
|
├── package.json # 前端依赖配置
|
||||||
@@ -106,8 +104,8 @@ Thing/
|
|||||||
- [ ] 网速监控
|
- [ ] 网速监控
|
||||||
|
|
||||||
#### ⬇️ 下载器
|
#### ⬇️ 下载器
|
||||||
- [x] 下载/内置 aria2 内核集成
|
- [x] 自建进程内下载引擎(多线程 HTTP/HTTPS,无需外部内核)
|
||||||
- [x] 接管浏览器下载,创建edge插件(Thing Extension)?
|
- [x] 接管浏览器下载,浏览器扩展(Thing Extension)
|
||||||
- [x] HTTP 下载支持
|
- [x] HTTP 下载支持
|
||||||
- [ ] BT/磁力链接支持
|
- [ ] BT/磁力链接支持
|
||||||
- [x] 下载任务管理(历史)
|
- [x] 下载任务管理(历史)
|
||||||
@@ -168,7 +166,7 @@ export const moduleConfig: ModuleConfig = {
|
|||||||
|
|
||||||
### 进程管理架构
|
### 进程管理架构
|
||||||
|
|
||||||
需要管理外部子进程的模块(如代理的 mihomo、下载器的 aria2)通过 `ModuleProcessConfig` 声明进程配置,由 Rust 端的 `ProcessManager` 统一管理生命周期:
|
需要管理外部子进程的模块(如代理的 mihomo)通过 `ModuleProcessConfig` 声明进程配置,由 Rust 端的 `ProcessManager` 统一管理生命周期。下载器模块使用进程内自建下载引擎,无需外部子进程:
|
||||||
|
|
||||||
- **Rust 端** (`src-tauri/src/process_manager.rs`):管理子进程的启动、停止、重启和崩溃检测
|
- **Rust 端** (`src-tauri/src/process_manager.rs`):管理子进程的启动、停止、重启和崩溃检测
|
||||||
- **前端** (`src/stores/processStore.ts`):Pinia store,通过 Tauri IPC 调用 Rust 命令,监听进程状态变更事件
|
- **前端** (`src/stores/processStore.ts`):Pinia store,通过 Tauri IPC 调用 Rust 命令,监听进程状态变更事件
|
||||||
|
|||||||
Generated
+17
@@ -4122,6 +4122,22 @@ dependencies = [
|
|||||||
"zbus",
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-snap-layout"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4f76070bb989b055b9c8a789520c3e2eb70f6cb155fc70598d464925e36a1d78"
|
||||||
|
dependencies = [
|
||||||
|
"raw-window-handle",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
"windows-version",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.11.3"
|
version = "2.11.3"
|
||||||
@@ -4261,6 +4277,7 @@ dependencies = [
|
|||||||
"tauri-plugin-autostart",
|
"tauri-plugin-autostart",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
|
"tauri-plugin-snap-layout",
|
||||||
"tokio",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ tauri-build = { version = "2", features = [] }
|
|||||||
tauri = { version = "2", features = ["tray-icon"] }
|
tauri = { version = "2", features = ["tray-icon"] }
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
|
tauri-plugin-snap-layout = "1"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
"core:window:allow-set-effects",
|
"core:window:allow-set-effects",
|
||||||
"core:window:allow-set-background-color",
|
"core:window:allow-set-background-color",
|
||||||
"core:window:allow-set-theme"
|
"core:window:allow-set-theme",
|
||||||
|
"snap-layout:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -158,9 +158,10 @@ impl DownloadEngine {
|
|||||||
|
|
||||||
let id = self.inner.storage.next_task_id();
|
let id = self.inner.storage.next_task_id();
|
||||||
|
|
||||||
// 创建分段
|
// 创建分段(受 continue_download 设置控制:关闭时强制单线程、不支持续传)
|
||||||
|
let resume_enabled = settings.continue_download;
|
||||||
let segments = match &probe {
|
let segments = match &probe {
|
||||||
Ok(p) if p.supports_resume && p.total_size.map(|s| s > 0).unwrap_or(false) => {
|
Ok(p) if resume_enabled && p.supports_resume && p.total_size.map(|s| s > 0).unwrap_or(false) => {
|
||||||
split_segments(p.total_size.unwrap(), settings.max_connections)
|
split_segments(p.total_size.unwrap(), settings.max_connections)
|
||||||
}
|
}
|
||||||
_ => vec![Segment {
|
_ => vec![Segment {
|
||||||
@@ -172,7 +173,8 @@ impl DownloadEngine {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let total_size = probe.as_ref().ok().and_then(|p| p.total_size).unwrap_or(0);
|
let total_size = probe.as_ref().ok().and_then(|p| p.total_size).unwrap_or(0);
|
||||||
let supports_resume = probe.as_ref().ok().map(|p| p.supports_resume).unwrap_or(false);
|
let supports_resume = resume_enabled
|
||||||
|
&& probe.as_ref().ok().map(|p| p.supports_resume).unwrap_or(false);
|
||||||
|
|
||||||
let task = DownloadTask {
|
let task = DownloadTask {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
|
|||||||
@@ -318,13 +318,14 @@ async fn download_segment_with_client(
|
|||||||
match stream.next().await {
|
match stream.next().await {
|
||||||
Some(Ok(chunk)) => {
|
Some(Ok(chunk)) => {
|
||||||
buf.extend_from_slice(&chunk);
|
buf.extend_from_slice(&chunk);
|
||||||
|
// 接收到数据立即更新进度(避免监控周期内进度无变化导致速度显示为 0)
|
||||||
|
local_completed += chunk.len() as u64;
|
||||||
|
progress.store(local_completed, Ordering::Relaxed);
|
||||||
// 积累到 64KB 再写入(减少 I/O 次数)
|
// 积累到 64KB 再写入(减少 I/O 次数)
|
||||||
if buf.len() >= 64 * 1024 {
|
if buf.len() >= 64 * 1024 {
|
||||||
file.write_all(&buf)
|
file.write_all(&buf)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("写入文件失败: {}", e))?;
|
.map_err(|e| format!("写入文件失败: {}", e))?;
|
||||||
local_completed += buf.len() as u64;
|
|
||||||
progress.store(local_completed, Ordering::Relaxed);
|
|
||||||
// 限速
|
// 限速
|
||||||
limiter.consume(buf.len() as u64).await;
|
limiter.consume(buf.len() as u64).await;
|
||||||
buf.clear();
|
buf.clear();
|
||||||
@@ -339,8 +340,7 @@ async fn download_segment_with_client(
|
|||||||
file.write_all(&buf)
|
file.write_all(&buf)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("写入文件失败: {}", e))?;
|
.map_err(|e| format!("写入文件失败: {}", e))?;
|
||||||
local_completed += buf.len() as u64;
|
// local_completed 和 progress 已在接收 chunk 时更新
|
||||||
progress.store(local_completed, Ordering::Relaxed);
|
|
||||||
limiter.consume(buf.len() as u64).await;
|
limiter.consume(buf.len() as u64).await;
|
||||||
buf.clear();
|
buf.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ pub struct Storage {
|
|||||||
|
|
||||||
impl Storage {
|
impl Storage {
|
||||||
pub fn new(data_dir: PathBuf) -> Self {
|
pub fn new(data_dir: PathBuf) -> Self {
|
||||||
|
// 确保数据目录存在(首次启动或目录被删除时自动创建)
|
||||||
|
if let Err(e) = fs::create_dir_all(&data_dir) {
|
||||||
|
eprintln!("[download_engine] 创建数据目录失败: {} ({})", data_dir.display(), e);
|
||||||
|
}
|
||||||
let state_path = data_dir.join("engine_state.json");
|
let state_path = data_dir.join("engine_state.json");
|
||||||
let existing = Self::load_raw(&state_path);
|
let existing = Self::load_raw(&state_path);
|
||||||
let next_id = existing.as_ref().map(|s| s.next_id).unwrap_or(1);
|
let next_id = existing.as_ref().map(|s| s.next_id).unwrap_or(1);
|
||||||
@@ -70,6 +74,10 @@ impl Storage {
|
|||||||
state.next_id = self.id_counter.load(Ordering::SeqCst);
|
state.next_id = self.id_counter.load(Ordering::SeqCst);
|
||||||
match serde_json::to_string_pretty(&state) {
|
match serde_json::to_string_pretty(&state) {
|
||||||
Ok(json) => {
|
Ok(json) => {
|
||||||
|
// 兜底:若父目录被外部删除则在写入前重建
|
||||||
|
if let Some(parent) = self.state_path.parent() {
|
||||||
|
let _ = fs::create_dir_all(parent);
|
||||||
|
}
|
||||||
if let Err(e) = fs::write(&self.state_path, json) {
|
if let Err(e) = fs::write(&self.state_path, json) {
|
||||||
eprintln!("[download_engine] 保存状态失败: {}", e);
|
eprintln!("[download_engine] 保存状态失败: {}", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_autostart::Builder::new().build())
|
.plugin(tauri_plugin_autostart::Builder::new().build())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
|
.plugin(
|
||||||
|
tauri_plugin_snap_layout::init()
|
||||||
|
.button_id("titlebar-maximize")
|
||||||
|
.build()
|
||||||
|
)
|
||||||
.manage(ProcessManager::new())
|
.manage(ProcessManager::new())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
greet,
|
greet,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { Search, Minus, Square, X, Settings, ChevronRight } from '@lucide/vue'
|
import { Search, Minus, X, Settings, ChevronRight } from '@lucide/vue'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
import { useSearchStore, type SearchItem } from '@/stores/searchStore'
|
import { useSearchStore, type SearchItem } from '@/stores/searchStore'
|
||||||
@@ -64,6 +64,10 @@ const minimize = async () => {
|
|||||||
await tauriWindow?.minimize()
|
await tauriWindow?.minimize()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 窗口最大化状态:切换最大化/还原图标
|
||||||
|
const isMaximized = ref(false)
|
||||||
|
let unlistenMaximize: (() => void) | null = null
|
||||||
|
|
||||||
const maximize = async () => {
|
const maximize = async () => {
|
||||||
await tauriWindow?.toggleMaximize()
|
await tauriWindow?.toggleMaximize()
|
||||||
}
|
}
|
||||||
@@ -119,9 +123,9 @@ if (tauriWindow) {
|
|||||||
if (document.activeElement instanceof HTMLElement) {
|
if (document.activeElement instanceof HTMLElement) {
|
||||||
document.activeElement.blur()
|
document.activeElement.blur()
|
||||||
}
|
}
|
||||||
// 200ms 后恢复 hover,足够让浏览器重置伪类状态
|
// 不使用定时器恢复 hover:窗口从隐藏恢复显示时,浏览器 :hover 可能被冻结
|
||||||
restoreHover(200)
|
// (mouseleave 在窗口隐藏时不触发),定时器恢复会让冻结的 hover 提前暴露
|
||||||
// 同时注册鼠标移动监听作为后备(定时器可能不够,鼠标移动更可靠)
|
// 仅依赖鼠标移动恢复——移动时浏览器才会重新评估 :hover 状态
|
||||||
armMouseMoveRestore()
|
armMouseMoveRestore()
|
||||||
} else {
|
} else {
|
||||||
hoverSuppressed.value = true
|
hoverSuppressed.value = true
|
||||||
@@ -132,9 +136,23 @@ if (tauriWindow) {
|
|||||||
// 初始时窗口已显示,恢复 hover
|
// 初始时窗口已显示,恢复 hover
|
||||||
restoreHover(300)
|
restoreHover(300)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (tauriWindow) {
|
||||||
|
try {
|
||||||
|
isMaximized.value = await tauriWindow.isMaximized()
|
||||||
|
unlistenMaximize = await tauriWindow.onResized(async () => {
|
||||||
|
isMaximized.value = await tauriWindow!.isMaximized()
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
// 非 Tauri 环境忽略
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('mousemove', handleFirstMouseMove)
|
window.removeEventListener('mousemove', handleFirstMouseMove)
|
||||||
if (restoreHoverTimer) clearTimeout(restoreHoverTimer)
|
if (restoreHoverTimer) clearTimeout(restoreHoverTimer)
|
||||||
|
if (unlistenMaximize) unlistenMaximize()
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleBlur = () => {
|
const handleBlur = () => {
|
||||||
@@ -241,21 +259,30 @@ const handleBlur = () => {
|
|||||||
|
|
||||||
<div class="flex items-center pointer-events-auto" :class="{ 'hover-suppressed': hoverSuppressed }">
|
<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-foreground/5 transition-colors rounded-sm"
|
||||||
@click="minimize"
|
@click="minimize"
|
||||||
@mousedown.stop
|
@mousedown.stop
|
||||||
>
|
>
|
||||||
<Minus class="h-4 w-4" />
|
<Minus class="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="h-10 w-10 flex items-center justify-center hover:bg-secondary/50 transition-colors rounded-sm"
|
id="titlebar-maximize"
|
||||||
|
class="h-10 w-10 flex items-center justify-center transition-colors rounded-sm titlebar-maximize-btn"
|
||||||
@click="maximize"
|
@click="maximize"
|
||||||
@mousedown.stop
|
@mousedown.stop
|
||||||
>
|
>
|
||||||
<Square class="h-3.5 w-3.5" />
|
<!-- 最大化:单圆角矩形 -->
|
||||||
|
<svg v-if="!isMaximized" class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="4" y="4" width="16" height="16" rx="2" />
|
||||||
|
</svg>
|
||||||
|
<!-- 还原:单圆角矩形 + 右上角圆角 L形(顶边+右边,圆角连接) -->
|
||||||
|
<svg v-else class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="4" y="4" width="16" height="16" rx="2" />
|
||||||
|
<path d="M14 4H20V10" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="h-10 w-10 flex items-center justify-center hover:bg-destructive/20 transition-colors rounded-sm text-destructive"
|
class="h-10 w-10 flex items-center justify-center hover:bg-red-500 hover:text-white text-foreground transition-colors rounded-sm"
|
||||||
@click="close"
|
@click="close"
|
||||||
@mousedown.stop
|
@mousedown.stop
|
||||||
>
|
>
|
||||||
@@ -267,10 +294,17 @@ const handleBlur = () => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 窗口隐藏/重新显示瞬间,抑制按钮 hover 样式,避免冻结的 :hover 残留 */
|
/* 窗口隐藏/重新显示瞬间,抑制按钮 hover 样式,避免冻结的 :hover 残留 */
|
||||||
.hover-suppressed button:hover {
|
.hover-suppressed button:hover,
|
||||||
|
.hover-suppressed button.is-hovered {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 最大化按钮:snap-layout 插件的原生子窗口遮挡了指针事件,
|
||||||
|
:hover 无法触发,改用插件注入的 .is-hovered 类(鼠标进入原生子窗口时添加) */
|
||||||
|
.titlebar-maximize-btn.is-hovered {
|
||||||
|
background-color: color-mix(in srgb, var(--foreground) 5%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
/* 浮动标签切换器进出动画:淡入 + 从左侧滑入 */
|
/* 浮动标签切换器进出动画:淡入 + 从左侧滑入 */
|
||||||
.floating-tabs-enter-active,
|
.floating-tabs-enter-active,
|
||||||
.floating-tabs-leave-active {
|
.floating-tabs-leave-active {
|
||||||
|
|||||||
@@ -640,17 +640,17 @@ const toggleSortOrder = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 shrink-0">
|
<div class="flex items-center gap-1 shrink-0">
|
||||||
<Button
|
<Button
|
||||||
v-if="task.status === 'active'"
|
v-if="task.status === 'active' || task.status === 'queued'"
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="h-8 w-8"
|
class="h-8 w-8"
|
||||||
title="暂停"
|
:title="task.status === 'queued' ? '取消排队' : '暂停'"
|
||||||
@click="handlePause(task.id)"
|
@click="handlePause(task.id)"
|
||||||
>
|
>
|
||||||
<Pause class="h-4 w-4" />
|
<Pause class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
v-if="task.status === 'paused' || task.status === 'queued' || task.status === 'error'"
|
v-if="task.status === 'paused' || task.status === 'error'"
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="h-8 w-8"
|
class="h-8 w-8"
|
||||||
@@ -1099,7 +1099,7 @@ const toggleSortOrder = () => {
|
|||||||
<Textarea
|
<Textarea
|
||||||
v-model="addUriText"
|
v-model="addUriText"
|
||||||
placeholder="https://example.com/file.zip https://example.com/file2.zip"
|
placeholder="https://example.com/file.zip https://example.com/file2.zip"
|
||||||
class="min-h-[120px] font-mono text-sm"
|
class="min-h-[120px] font-mono text-sm [field-sizing:fixed] break-all"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -558,6 +558,18 @@ const handleCheckUpdate = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleUpdateKernel = async () => {
|
const handleUpdateKernel = async () => {
|
||||||
|
// 展开更新区块(下载源选择 + 进度),由 handleStartUpdate 执行实际更新
|
||||||
|
updateExpanded.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否展开"更新内核"区块(下载源 + 进度) */
|
||||||
|
const updateExpanded = ref(false)
|
||||||
|
|
||||||
|
/** 开始更新:停止 mihomo → 调用 updateKernel(复用 installProgress 进度机制) */
|
||||||
|
const handleStartUpdate = async () => {
|
||||||
|
if (store.installing) return
|
||||||
|
// 确认停止 mihomo
|
||||||
|
if (running.value) {
|
||||||
const ok = await showConfirm({
|
const ok = await showConfirm({
|
||||||
title: '更新内核',
|
title: '更新内核',
|
||||||
description: '更新内核需要先停止 mihomo,确认继续?',
|
description: '更新内核需要先停止 mihomo,确认继续?',
|
||||||
@@ -566,19 +578,21 @@ const handleUpdateKernel = async () => {
|
|||||||
if (!ok) return
|
if (!ok) return
|
||||||
updatingKernel.value = true
|
updatingKernel.value = true
|
||||||
try {
|
try {
|
||||||
// 先停止
|
|
||||||
if (running.value) {
|
|
||||||
await store.stop()
|
await store.stop()
|
||||||
}
|
|
||||||
toast.info('正在下载并安装内核...')
|
|
||||||
await store.updateKernel()
|
|
||||||
toast.success('内核更新完成')
|
|
||||||
kernelUpdateInfo.value = null
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error('内核更新失败', { description: String(e) })
|
toast.error('停止 mihomo 失败', { description: String(e) })
|
||||||
|
updatingKernel.value = false
|
||||||
|
return
|
||||||
} finally {
|
} finally {
|
||||||
updatingKernel.value = false
|
updatingKernel.value = false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
toast.info('开始下载更新...')
|
||||||
|
try {
|
||||||
|
await store.updateKernel(selectedMirrorPrefix.value)
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('内核更新失败', { description: String(e) })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 首次安装内核 =====
|
// ===== 首次安装内核 =====
|
||||||
@@ -653,7 +667,8 @@ const handleInstallKernel = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听安装进度终态,弹 toast 并延时清空进度
|
// 监听安装/更新进度终态,弹 toast 并延时清空进度
|
||||||
|
// 同时处理更新场景下的 updateExpanded 清理(与 installProgress 同步清除,避免更新区块闪烁)
|
||||||
watch(
|
watch(
|
||||||
() => store.installProgress?.stage,
|
() => store.installProgress?.stage,
|
||||||
(stage) => {
|
(stage) => {
|
||||||
@@ -661,13 +676,22 @@ watch(
|
|||||||
toast.success('内核安装完成', {
|
toast.success('内核安装完成', {
|
||||||
description: store.installProgress?.message
|
description: store.installProgress?.message
|
||||||
})
|
})
|
||||||
// 2 秒后清空进度,让用户看到 100% 终态
|
if (updateExpanded.value) {
|
||||||
setTimeout(() => store.clearInstallProgress(), 2000)
|
kernelUpdateInfo.value = null
|
||||||
|
}
|
||||||
|
// 2 秒后同时清空进度和更新区块,让用户看到 100% 终态
|
||||||
|
setTimeout(() => {
|
||||||
|
store.clearInstallProgress()
|
||||||
|
updateExpanded.value = false
|
||||||
|
}, 2000)
|
||||||
} else if (stage === 'error') {
|
} else if (stage === 'error') {
|
||||||
toast.error('内核安装失败', {
|
toast.error('内核安装失败', {
|
||||||
description: store.installProgress?.message
|
description: store.installProgress?.message
|
||||||
})
|
})
|
||||||
setTimeout(() => store.clearInstallProgress(), 5000)
|
setTimeout(() => {
|
||||||
|
store.clearInstallProgress()
|
||||||
|
// 错误时不清除 updateExpanded,让用户可以重新选择下载源重试
|
||||||
|
}, 5000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -912,11 +936,10 @@ const saveSettingsForm = async () => {
|
|||||||
v-if="kernelUpdateInfo.hasUpdate"
|
v-if="kernelUpdateInfo.hasUpdate"
|
||||||
key="btn-update"
|
key="btn-update"
|
||||||
size="xs" variant="default"
|
size="xs" variant="default"
|
||||||
:disabled="updatingKernel"
|
:disabled="store.installing || updateExpanded"
|
||||||
@click="handleUpdateKernel"
|
@click="handleUpdateKernel"
|
||||||
>
|
>
|
||||||
<Loader2 v-if="updatingKernel" class="size-3 animate-spin" />
|
<Download key="icon-download" class="size-3" />更新
|
||||||
<Download v-else key="icon-download" class="size-3" />更新
|
|
||||||
</Button>
|
</Button>
|
||||||
<Check v-else key="icon-updated" class="size-3 text-emerald-500" />
|
<Check v-else key="icon-updated" class="size-3 text-emerald-500" />
|
||||||
</span>
|
</span>
|
||||||
@@ -941,6 +964,61 @@ const saveSettingsForm = async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 更新内核区块:检查到更新且用户点击"更新"后展开(复用首次安装的下载源选择 UI) -->
|
||||||
|
<div
|
||||||
|
v-if="updateExpanded && !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>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm" variant="outline" class="flex-1"
|
||||||
|
:disabled="store.installing || updatingKernel"
|
||||||
|
@click="updateExpanded = false"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm" variant="default" class="flex-1"
|
||||||
|
:disabled="store.installing || updatingKernel || (mirrorChoice === '__custom' && !customMirror.trim())"
|
||||||
|
@click="handleStartUpdate"
|
||||||
|
>
|
||||||
|
<Loader2 v-if="store.installing || updatingKernel" class="size-4 animate-spin" />
|
||||||
|
<DownloadCloud v-else class="size-4" />开始更新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 首次安装区块:仅在内核未安装且不在安装中且已初始化时显示 -->
|
<!-- 首次安装区块:仅在内核未安装且不在安装中且已初始化时显示 -->
|
||||||
<div
|
<div
|
||||||
v-if="store.initialized && !store.kernel?.exists && !store.installProgress"
|
v-if="store.initialized && !store.kernel?.exists && !store.installProgress"
|
||||||
|
|||||||
@@ -272,9 +272,39 @@ export const useProxyStore = defineStore('proxy', () => {
|
|||||||
return await invoke<KernelUpdateInfo>('proxy_check_kernel_update')
|
return await invoke<KernelUpdateInfo>('proxy_check_kernel_update')
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateKernel = async (mirrorPrefix: string = '') => {
|
/**
|
||||||
|
* 更新内核:复用 installKernel 的完整进度事件机制(installing/installProgress/事件监听)
|
||||||
|
* 与 installKernel 的区别仅在于后端会先 stop mihomo(由调用方在前端控制),
|
||||||
|
* 后端 proxy_update_kernel 与 proxy_install_kernel 共用 install_kernel 实现
|
||||||
|
*/
|
||||||
|
const updateKernel = 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_update_kernel', { mirrorPrefix })
|
await invoke('proxy_update_kernel', { mirrorPrefix })
|
||||||
await refreshKernel()
|
await refreshKernel()
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('内核更新失败: ' + e)
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
installing.value = false
|
||||||
|
if (progressUnlisten) {
|
||||||
|
progressUnlisten()
|
||||||
|
progressUnlisten = null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ export type ModuleCategory = 'network' | 'tool' | 'system' | 'media'
|
|||||||
|
|
||||||
/** 模块进程配置 —— 需要管理外部子进程的模块填写 */
|
/** 模块进程配置 —— 需要管理外部子进程的模块填写 */
|
||||||
export interface ModuleProcessConfig {
|
export interface ModuleProcessConfig {
|
||||||
/** 进程标识符(如 'mihomo'、'aria2') */
|
/** 进程标识符(如 'mihomo') */
|
||||||
name: string
|
name: string
|
||||||
/** 可执行文件路径(运行时由模块自行确定) */
|
/** 可执行文件路径(运行时由模块自行确定) */
|
||||||
executable: string
|
executable: string
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ export default defineConfig(async () => ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 防止 Vite 缓存 tauri-plugin-snap-layout,否则注入脚本中的占位符无法被替换
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: ["tauri-plugin-snap-layout"],
|
||||||
|
},
|
||||||
|
|
||||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||||
//
|
//
|
||||||
// 1. prevent Vite from obscuring rust errors
|
// 1. prevent Vite from obscuring rust errors
|
||||||
|
|||||||
Reference in New Issue
Block a user