下载非内核
This commit is contained in:
+31
-4
@@ -25,7 +25,9 @@ const toNavModule = (meta: ModuleMeta): NavModule => ({
|
||||
icon: meta.icon
|
||||
})
|
||||
|
||||
const activeModule = ref('proxy')
|
||||
/** 上次激活的模块 ID(localStorage 持久化) */
|
||||
const LAST_MODULE_KEY = 'thing_last_module'
|
||||
const activeModule = ref('')
|
||||
|
||||
const activeComponent = shallowRef<Component | null>(null)
|
||||
|
||||
@@ -64,11 +66,13 @@ const handleModuleChange = (moduleId: string) => {
|
||||
prevConfig?.lifecycle?.onDeactivate?.()
|
||||
|
||||
activeModule.value = moduleId
|
||||
localStorage.setItem(LAST_MODULE_KEY, moduleId)
|
||||
loadModule(moduleId)
|
||||
}
|
||||
|
||||
const handleSearch = (moduleId: string) => {
|
||||
activeModule.value = moduleId
|
||||
localStorage.setItem(LAST_MODULE_KEY, moduleId)
|
||||
loadModule(moduleId)
|
||||
}
|
||||
|
||||
@@ -81,6 +85,8 @@ const getFallbackModule = () => {
|
||||
}
|
||||
|
||||
watch(() => appStore.enabledModules.length, () => {
|
||||
// 启动期间 activeModule 尚未确定,跳过
|
||||
if (!activeModule.value) return
|
||||
const enabledIds = appStore.enabledModules.map(m => m.id)
|
||||
if (activeModule.value !== 'settings' && !enabledIds.includes(activeModule.value)) {
|
||||
const fallback = getFallbackModule()
|
||||
@@ -89,9 +95,30 @@ watch(() => appStore.enabledModules.length, () => {
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadModule(activeModule.value)
|
||||
appStore.init().catch(e => console.error('App init error:', e))
|
||||
/** 计算启动时应打开的默认模块:优先上次记忆,其次排序第一个 */
|
||||
const resolveDefaultModule = (): string => {
|
||||
const enabledIds = appStore.enabledModules.map(m => m.id)
|
||||
const allMetas = moduleRegistry.getAllMetas()
|
||||
|
||||
// 尝试上次记忆的模块
|
||||
const lastModule = localStorage.getItem(LAST_MODULE_KEY)
|
||||
if (lastModule) {
|
||||
const meta = allMetas.find(m => m.id === lastModule)
|
||||
if (meta && (meta.builtin || enabledIds.includes(lastModule))) {
|
||||
return lastModule
|
||||
}
|
||||
}
|
||||
|
||||
// 回退到排序第一个可用模块(settings 除外)
|
||||
const first = availableModules.value.find(m => m.id !== 'settings')
|
||||
return first?.id || 'settings'
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await appStore.init().catch(e => console.error('App init error:', e))
|
||||
const defaultModule = resolveDefaultModule()
|
||||
activeModule.value = defaultModule
|
||||
loadModule(defaultModule)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onUnmounted } from 'vue'
|
||||
import { Search, Minus, Square, X, Settings, ChevronRight } from '@lucide/vue'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
@@ -72,17 +72,46 @@ const maximize = async () => {
|
||||
// 期间用 CSS 覆盖 :hover 样式,避免按钮残留高亮(webview 隐藏时 mouseleave 不触发)
|
||||
const hoverSuppressed = ref(true)
|
||||
|
||||
// 恢复 hover 的定时器引用,便于在鼠标进入时提前恢复
|
||||
let restoreHoverTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
/** 窗口重新显示后恢复 hover 状态 */
|
||||
const restoreHover = (delay = 200) => {
|
||||
if (restoreHoverTimer) clearTimeout(restoreHoverTimer)
|
||||
restoreHoverTimer = setTimeout(() => {
|
||||
hoverSuppressed.value = false
|
||||
}, delay)
|
||||
}
|
||||
|
||||
/** 鼠标移动时恢复 hover(比定时器更可靠,webview 隐藏时 mouseleave 不触发,:hover 被冻结) */
|
||||
const handleFirstMouseMove = () => {
|
||||
if (hoverSuppressed.value) {
|
||||
hoverSuppressed.value = false
|
||||
if (restoreHoverTimer) {
|
||||
clearTimeout(restoreHoverTimer)
|
||||
restoreHoverTimer = null
|
||||
}
|
||||
}
|
||||
window.removeEventListener('mousemove', handleFirstMouseMove)
|
||||
}
|
||||
|
||||
/** 注册鼠标移动监听(窗口即将隐藏时调用,为下次显示做准备) */
|
||||
const armMouseMoveRestore = () => {
|
||||
window.removeEventListener('mousemove', handleFirstMouseMove)
|
||||
window.addEventListener('mousemove', handleFirstMouseMove)
|
||||
}
|
||||
|
||||
const close = async () => {
|
||||
// 隐藏前立即抑制 hover,避免冻结的 :hover 状态残留到下次显示
|
||||
hoverSuppressed.value = true
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur()
|
||||
}
|
||||
// 注册鼠标移动监听,窗口重新显示后第一次移动即恢复 hover
|
||||
armMouseMoveRestore()
|
||||
await tauriWindow?.hide()
|
||||
}
|
||||
|
||||
// 窗口重新获得焦点时,先保持抑制(防止冻结的 hover 显示),
|
||||
// 短暂延迟后恢复 hover(让真实鼠标位置重新接管)
|
||||
if (tauriWindow) {
|
||||
tauriWindow.onFocusChanged(({ payload: focused }) => {
|
||||
if (focused) {
|
||||
@@ -90,10 +119,10 @@ if (tauriWindow) {
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur()
|
||||
}
|
||||
// 100ms 后恢复 hover,足够让浏览器重置伪类状态
|
||||
setTimeout(() => {
|
||||
hoverSuppressed.value = false
|
||||
}, 100)
|
||||
// 200ms 后恢复 hover,足够让浏览器重置伪类状态
|
||||
restoreHover(200)
|
||||
// 同时注册鼠标移动监听作为后备(定时器可能不够,鼠标移动更可靠)
|
||||
armMouseMoveRestore()
|
||||
} else {
|
||||
hoverSuppressed.value = true
|
||||
}
|
||||
@@ -101,9 +130,12 @@ if (tauriWindow) {
|
||||
}
|
||||
|
||||
// 初始时窗口已显示,恢复 hover
|
||||
setTimeout(() => {
|
||||
hoverSuppressed.value = false
|
||||
}, 200)
|
||||
restoreHover(300)
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('mousemove', handleFirstMouseMove)
|
||||
if (restoreHoverTimer) clearTimeout(restoreHoverTimer)
|
||||
})
|
||||
|
||||
const handleBlur = () => {
|
||||
setTimeout(() => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,11 @@
|
||||
import type { ModuleConfig } from '@/types/module'
|
||||
import type { SearchIndexItem } from '@/stores/searchIndex'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
const searchItems: SearchIndexItem[] = [
|
||||
{
|
||||
title: '下载任务',
|
||||
description: '查看与管理下载任务',
|
||||
keywords: ['下载', 'download', '任务', 'task', 'aria2']
|
||||
keywords: ['下载', 'download', '任务', 'task']
|
||||
},
|
||||
{
|
||||
title: '添加下载',
|
||||
@@ -15,8 +14,8 @@ const searchItems: SearchIndexItem[] = [
|
||||
},
|
||||
{
|
||||
title: '下载设置',
|
||||
description: '配置下载目录、速度限制与 RPC',
|
||||
keywords: ['设置', 'setting', 'rpc', '速度', '端口', '目录']
|
||||
description: '配置下载目录、并发数与速度限制',
|
||||
keywords: ['设置', 'setting', '速度', '目录', '并发']
|
||||
},
|
||||
{
|
||||
title: '浏览器扩展',
|
||||
@@ -29,39 +28,18 @@ export const moduleConfig: ModuleConfig = {
|
||||
id: 'downloader',
|
||||
name: '下载器',
|
||||
icon: 'downloader',
|
||||
description: '基于 aria2 的多线程 HTTP 下载管理',
|
||||
description: '多线程 HTTP 下载管理',
|
||||
category: 'network',
|
||||
defaultEnabled: true,
|
||||
loader: () => import('./DownloaderModule.vue'),
|
||||
searchItems,
|
||||
// 进程由 Aria2Manager 通过 ProcessManager 统一管理(id='downloader'),
|
||||
// executable/args 在运行时由后端确定,此处仅声明 hasProcess 以便禁用时自动停止。
|
||||
process: {
|
||||
name: 'aria2c',
|
||||
executable: '',
|
||||
autoStart: false,
|
||||
restartOnCrash: true,
|
||||
maxRestarts: 3
|
||||
},
|
||||
// 下载引擎在进程内运行,无需外部进程管理
|
||||
lifecycle: {
|
||||
// 启用模块时若用户开启了"自动启动",则随模块启用而运行 aria2
|
||||
onEnable: async () => {
|
||||
try {
|
||||
const s = await invoke<{ autoStart?: boolean }>('downloader_get_settings')
|
||||
if (s.autoStart) {
|
||||
await invoke('downloader_start')
|
||||
}
|
||||
} catch {
|
||||
/* 忽略:可能内核未安装 */
|
||||
}
|
||||
// 引擎在应用启动时已自动运行,模块启用时无需额外操作
|
||||
},
|
||||
// 禁用模块时停止 aria2 进程(cleanup_on_exit 会在应用退出时调用)
|
||||
onDisable: async () => {
|
||||
try {
|
||||
await invoke('downloader_stop')
|
||||
} catch {
|
||||
/* 忽略:可能进程未运行 */
|
||||
}
|
||||
// 模块禁用时不停止引擎(引擎在应用退出时统一清理)
|
||||
}
|
||||
},
|
||||
order: 50
|
||||
|
||||
@@ -322,6 +322,7 @@ const init = async () => {
|
||||
startAutoSwitch()
|
||||
}
|
||||
}
|
||||
store.initialized = true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -883,12 +884,19 @@ const saveSettingsForm = async () => {
|
||||
<CardContent class="space-y-3 text-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-muted-foreground">状态</span>
|
||||
<Badge v-if="store.kernel?.exists" variant="default" class="gap-1 bg-emerald-500 hover:bg-emerald-500">
|
||||
<Check class="size-3" />已安装
|
||||
</Badge>
|
||||
<Badge v-else variant="destructive" class="gap-1">
|
||||
<AlertCircle class="size-3" />未安装
|
||||
</Badge>
|
||||
<template v-if="!store.initialized">
|
||||
<span class="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Loader2 class="size-3 animate-spin" />加载中...
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Badge v-if="store.kernel?.exists" variant="default" class="gap-1 bg-emerald-500 hover:bg-emerald-500">
|
||||
<Check class="size-3" />已安装
|
||||
</Badge>
|
||||
<Badge v-else variant="destructive" class="gap-1">
|
||||
<AlertCircle class="size-3" />未安装
|
||||
</Badge>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="store.kernel?.exists" class="flex items-center justify-between">
|
||||
<span class="text-muted-foreground">当前版本</span>
|
||||
@@ -933,9 +941,9 @@ const saveSettingsForm = async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 首次安装区块:仅在内核未安装且不在安装中时显示 -->
|
||||
<!-- 首次安装区块:仅在内核未安装且不在安装中且已初始化时显示 -->
|
||||
<div
|
||||
v-if="!store.kernel?.exists && !store.installProgress"
|
||||
v-if="store.initialized && !store.kernel?.exists && !store.installProgress"
|
||||
class="space-y-2 pt-2 border-t"
|
||||
>
|
||||
<div class="space-y-1.5">
|
||||
@@ -1035,12 +1043,19 @@ const saveSettingsForm = async () => {
|
||||
<CardContent class="space-y-3 text-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-muted-foreground">mihomo</span>
|
||||
<span v-if="running" class="flex items-center gap-1 text-emerald-500">
|
||||
<span class="size-2 rounded-full bg-emerald-500" />运行中
|
||||
</span>
|
||||
<span v-else class="flex items-center gap-1 text-muted-foreground">
|
||||
<span class="size-2 rounded-full bg-muted-foreground" />已停止
|
||||
</span>
|
||||
<template v-if="!store.initialized">
|
||||
<span class="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Loader2 class="size-3 animate-spin" />加载中...
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="running" class="flex items-center gap-1 text-emerald-500">
|
||||
<span class="size-2 rounded-full bg-emerald-500" />运行中
|
||||
</span>
|
||||
<span v-else class="flex items-center gap-1 text-muted-foreground">
|
||||
<span class="size-2 rounded-full bg-muted-foreground" />已停止
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-muted-foreground">PID</span>
|
||||
|
||||
+162
-321
@@ -8,285 +8,141 @@ const logger = createLogger('downloader')
|
||||
|
||||
// ===== 与 Rust 端对应的数据结构(camelCase) =====
|
||||
|
||||
export type TaskStatus = 'queued' | 'active' | 'paused' | 'complete' | 'error'
|
||||
|
||||
export interface Segment {
|
||||
index: number
|
||||
start: number
|
||||
end: number
|
||||
completed: number
|
||||
}
|
||||
|
||||
export interface DownloadTask {
|
||||
id: string
|
||||
url: string
|
||||
filename: string
|
||||
dir: string
|
||||
status: TaskStatus
|
||||
totalSize: number
|
||||
completedSize: number
|
||||
speed: number
|
||||
supportsResume: boolean
|
||||
segments: Segment[]
|
||||
error: string | null
|
||||
createdAt: number
|
||||
headers: Record<string, string>
|
||||
}
|
||||
|
||||
export interface DownloaderSettings {
|
||||
rpcPort: number
|
||||
rpcSecret: string
|
||||
downloadDir: string
|
||||
maxConcurrent: number
|
||||
maxConnectionPerServer: number
|
||||
split: number
|
||||
maxConnections: number
|
||||
continueDownload: boolean
|
||||
autoStart: boolean
|
||||
speedLimit: number
|
||||
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 KernelInfo {
|
||||
path: string
|
||||
exists: boolean
|
||||
version: string | null
|
||||
}
|
||||
|
||||
export interface KernelUpdateInfo {
|
||||
currentVersion: string | null
|
||||
latestVersion: string
|
||||
downloadUrl: string
|
||||
hasUpdate: boolean
|
||||
globalSpeedLimit: number
|
||||
extensionPort: number
|
||||
extensionSecret: string
|
||||
deleteFilesOnRemove: boolean
|
||||
}
|
||||
|
||||
export interface DownloaderStatus {
|
||||
running: boolean
|
||||
pid: number | null
|
||||
restartCount: number
|
||||
}
|
||||
|
||||
export interface RpcInfo {
|
||||
rpcUrl: string
|
||||
rpcSecret: string
|
||||
extensionPath: string | null
|
||||
export interface ExtensionInfo {
|
||||
url: string
|
||||
port: number
|
||||
secret: string
|
||||
hasSecret: boolean
|
||||
}
|
||||
|
||||
/** aria2 任务文件信息 */
|
||||
export interface Aria2File {
|
||||
path: string
|
||||
length: number
|
||||
completedLength: number
|
||||
selected: boolean
|
||||
/** 下载进度事件载荷 */
|
||||
interface ProgressPayload {
|
||||
id: string
|
||||
completedSize: number
|
||||
totalSize: number
|
||||
speed: number
|
||||
status: TaskStatus
|
||||
}
|
||||
|
||||
/** aria2 任务状态(tellActive/tellWaiting/tellStopped 返回项) */
|
||||
export interface Aria2Task {
|
||||
gid: string
|
||||
status: 'active' | 'waiting' | 'paused' | 'complete' | 'removed' | 'error' | string
|
||||
totalLength: string
|
||||
completedLength: string
|
||||
downloadSpeed: string
|
||||
uploadSpeed: string
|
||||
connections: string
|
||||
dir: string
|
||||
files?: Aria2File[]
|
||||
bittorrent?: { info?: { name?: string } } | null
|
||||
errorCode?: string
|
||||
errorMessage?: string
|
||||
}
|
||||
|
||||
/** 全局统计 */
|
||||
export interface GlobalStat {
|
||||
downloadSpeed: string
|
||||
uploadSpeed: string
|
||||
numActive: string
|
||||
numWaiting: string
|
||||
numStopped: string
|
||||
numStoppedTotal: string
|
||||
}
|
||||
|
||||
export interface Aria2Version {
|
||||
version: string
|
||||
enabledFeatures?: string[]
|
||||
/** 下载完成事件载荷 */
|
||||
interface CompletePayload {
|
||||
id: string
|
||||
filename: string
|
||||
status: TaskStatus
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export const useDownloaderStore = defineStore('downloader', () => {
|
||||
const kernel = ref<KernelInfo | null>(null)
|
||||
const status = ref<DownloaderStatus>({ running: false, pid: null, restartCount: 0 })
|
||||
const version = ref<string>('')
|
||||
const tasks = ref<DownloadTask[]>([])
|
||||
const status = ref<DownloaderStatus>({ running: false })
|
||||
const settings = ref<DownloaderSettings | null>(null)
|
||||
const rpcInfo = ref<RpcInfo | null>(null)
|
||||
const extensionInfo = ref<ExtensionInfo | null>(null)
|
||||
|
||||
const activeTasks = ref<Aria2Task[]>([])
|
||||
const waitingTasks = ref<Aria2Task[]>([])
|
||||
const stoppedTasks = ref<Aria2Task[]>([])
|
||||
const globalStat = ref<GlobalStat | null>(null)
|
||||
|
||||
// ===== 任务历史持久化(localStorage) =====
|
||||
// 即使 aria2 未启动,也能展示最近一次的任务快照
|
||||
const HISTORY_KEY = 'thing.downloader.taskHistory'
|
||||
const HISTORY_MAX = 200 // 最多保留 200 条历史记录
|
||||
|
||||
/** 将当前任务快照保存到 localStorage(合并 active+waiting+stopped,按 gid 去重) */
|
||||
const persistHistory = () => {
|
||||
try {
|
||||
const map = new Map<string, Aria2Task>()
|
||||
// 先读已有历史,作为基底
|
||||
const raw = localStorage.getItem(HISTORY_KEY)
|
||||
if (raw) {
|
||||
const existing: Aria2Task[] = JSON.parse(raw)
|
||||
for (const t of existing) map.set(t.gid, t)
|
||||
}
|
||||
// 用最新任务覆盖(active/waiting/stopped 都是最新的)
|
||||
for (const t of activeTasks.value) map.set(t.gid, t)
|
||||
for (const t of waitingTasks.value) map.set(t.gid, t)
|
||||
for (const t of stoppedTasks.value) map.set(t.gid, t)
|
||||
// 限制条数:优先保留 stopped(已完成/错误),其次 waiting,最后 active
|
||||
const all = Array.from(map.values())
|
||||
const priority = { complete: 0, error: 0, removed: 1, active: 2, waiting: 2, paused: 2 } as Record<string, number>
|
||||
all.sort((a, b) => (priority[a.status] ?? 3) - (priority[b.status] ?? 3))
|
||||
const trimmed = all.slice(0, HISTORY_MAX)
|
||||
localStorage.setItem(HISTORY_KEY, JSON.stringify(trimmed))
|
||||
} catch (e) {
|
||||
logger.error('保存任务历史失败: ' + e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 从 localStorage 加载任务历史,填充到 stoppedTasks(作为历史展示) */
|
||||
const loadHistory = () => {
|
||||
try {
|
||||
const raw = localStorage.getItem(HISTORY_KEY)
|
||||
if (!raw) return
|
||||
const history: Aria2Task[] = JSON.parse(raw)
|
||||
if (!Array.isArray(history)) return
|
||||
// 仅在没有实时任务时填充(避免覆盖实时数据)
|
||||
if (stoppedTasks.value.length === 0) {
|
||||
stoppedTasks.value = history
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('加载任务历史失败: ' + e)
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 内核安装进度 =====
|
||||
const installing = ref(false)
|
||||
const installProgress = ref<InstallProgress | null>(null)
|
||||
// 事件监听器
|
||||
let progressUnlisten: UnlistenFn | null = null
|
||||
let completeUnlisten: UnlistenFn | null = null
|
||||
let addedUnlisten: UnlistenFn | null = null
|
||||
|
||||
/** 内核信息(同时尝试从 resource 提取到 cores/) */
|
||||
const refreshKernel = async () => {
|
||||
// ===== 任务列表 =====
|
||||
const refreshTasks = async () => {
|
||||
try {
|
||||
kernel.value = await invoke<KernelInfo>('downloader_kernel_info')
|
||||
tasks.value = await invoke<DownloadTask[]>('downloader_get_tasks')
|
||||
} catch (e) {
|
||||
logger.error('获取内核信息失败: ' + e)
|
||||
logger.error('获取任务列表失败: ' + e)
|
||||
}
|
||||
return kernel.value
|
||||
return tasks.value
|
||||
}
|
||||
|
||||
/** 刷新进程状态 */
|
||||
const refreshStatus = async () => {
|
||||
try {
|
||||
status.value = await invoke<DownloaderStatus>('downloader_status')
|
||||
} catch (e) {
|
||||
logger.error('获取进程状态失败: ' + e)
|
||||
}
|
||||
return status.value
|
||||
}
|
||||
|
||||
const start = async () => {
|
||||
await invoke('downloader_start')
|
||||
await refreshStatus()
|
||||
}
|
||||
|
||||
const stop = async () => {
|
||||
await invoke('downloader_stop')
|
||||
await refreshStatus()
|
||||
}
|
||||
|
||||
const restart = async () => {
|
||||
await invoke('downloader_restart')
|
||||
await refreshStatus()
|
||||
}
|
||||
|
||||
/** 等待 aria2 RPC 就绪(轮询 version 接口,最多等 10 秒) */
|
||||
const waitForApi = async (timeoutMs = 10000): Promise<boolean> => {
|
||||
const start = Date.now()
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
try {
|
||||
await invoke<Aria2Version>('downloader_version')
|
||||
return true
|
||||
} catch {
|
||||
await new Promise((r) => setTimeout(r, 500))
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** 获取 aria2 版本(仅运行时可用) */
|
||||
const refreshVersion = async () => {
|
||||
try {
|
||||
const v = await invoke<Aria2Version>('downloader_version')
|
||||
version.value = v.version
|
||||
} catch {
|
||||
version.value = ''
|
||||
/** 根据进度事件更新单个任务(不触发完整刷新) */
|
||||
const updateTaskProgress = (payload: ProgressPayload) => {
|
||||
const task = tasks.value.find((t) => t.id === payload.id)
|
||||
if (task) {
|
||||
task.completedSize = payload.completedSize
|
||||
task.totalSize = payload.totalSize
|
||||
task.speed = payload.speed
|
||||
task.status = payload.status
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 任务列表 ----------
|
||||
const refreshActive = async () => {
|
||||
try {
|
||||
const res = await invoke<Aria2Task[]>('downloader_get_active')
|
||||
activeTasks.value = res ?? []
|
||||
} catch (e) {
|
||||
logger.error('获取活跃任务失败: ' + e)
|
||||
}
|
||||
return activeTasks.value
|
||||
/** 下载完成事件处理 */
|
||||
const handleTaskComplete = async (_payload: CompletePayload) => {
|
||||
// 刷新任务列表以获取最终状态
|
||||
await refreshTasks()
|
||||
}
|
||||
|
||||
const refreshWaiting = async () => {
|
||||
try {
|
||||
const res = await invoke<Aria2Task[]>('downloader_get_waiting')
|
||||
waitingTasks.value = res ?? []
|
||||
} catch (e) {
|
||||
logger.error('获取等待任务失败: ' + e)
|
||||
}
|
||||
return waitingTasks.value
|
||||
// ===== 任务操作 =====
|
||||
const addTask = async (
|
||||
url: string,
|
||||
filename?: string,
|
||||
dir?: string,
|
||||
headers?: Record<string, string>
|
||||
): Promise<string> => {
|
||||
const id = await invoke<string>('downloader_add_task', {
|
||||
url,
|
||||
filename: filename || null,
|
||||
dir: dir || null,
|
||||
headers: headers || null
|
||||
})
|
||||
await refreshTasks()
|
||||
return id
|
||||
}
|
||||
|
||||
const refreshStopped = async () => {
|
||||
try {
|
||||
const res = await invoke<Aria2Task[]>('downloader_get_stopped')
|
||||
stoppedTasks.value = res ?? []
|
||||
} catch (e) {
|
||||
logger.error('获取已完成任务失败: ' + e)
|
||||
}
|
||||
return stoppedTasks.value
|
||||
const pauseTask = async (id: string) => {
|
||||
await invoke('downloader_pause_task', { id })
|
||||
await refreshTasks()
|
||||
}
|
||||
|
||||
/** 刷新全部任务(active + waiting + stopped) */
|
||||
const refreshAllTasks = async () => {
|
||||
await Promise.all([refreshActive(), refreshWaiting(), refreshStopped()])
|
||||
// 刷新后持久化历史快照
|
||||
persistHistory()
|
||||
const resumeTask = async (id: string) => {
|
||||
await invoke('downloader_resume_task', { id })
|
||||
await refreshTasks()
|
||||
}
|
||||
|
||||
const refreshGlobalStat = async () => {
|
||||
try {
|
||||
globalStat.value = await invoke<GlobalStat>('downloader_get_global_stat')
|
||||
} catch (e) {
|
||||
logger.error('获取全局统计失败: ' + e)
|
||||
}
|
||||
return globalStat.value
|
||||
const removeTask = async (id: string, deleteFiles = false) => {
|
||||
await invoke('downloader_remove_task', { id, deleteFiles })
|
||||
await refreshTasks()
|
||||
}
|
||||
|
||||
// ---------- 任务操作 ----------
|
||||
const addUri = async (uris: string[], options?: Record<string, unknown>) => {
|
||||
const opts = options ? (JSON.parse(JSON.stringify(options)) as unknown) : undefined
|
||||
return await invoke<string>('downloader_add_uri', { uris, options: opts })
|
||||
}
|
||||
|
||||
const pauseTask = async (gid: string) => {
|
||||
await invoke('downloader_pause', { gid })
|
||||
}
|
||||
|
||||
const unpauseTask = async (gid: string) => {
|
||||
await invoke('downloader_unpause', { gid })
|
||||
}
|
||||
|
||||
const removeTask = async (gid: string) => {
|
||||
await invoke('downloader_remove', { gid })
|
||||
}
|
||||
|
||||
const changeGlobalOption = async (options: Record<string, string>) => {
|
||||
await invoke('downloader_change_global_option', { options })
|
||||
}
|
||||
|
||||
// ---------- 设置 ----------
|
||||
// ===== 设置 =====
|
||||
const loadSettings = async () => {
|
||||
settings.value = await invoke<DownloaderSettings>('downloader_get_settings')
|
||||
return settings.value
|
||||
@@ -297,107 +153,92 @@ export const useDownloaderStore = defineStore('downloader', () => {
|
||||
settings.value = s
|
||||
}
|
||||
|
||||
// ---------- RPC 信息 ----------
|
||||
const loadRpcInfo = async () => {
|
||||
rpcInfo.value = await invoke<RpcInfo>('downloader_get_rpc_info')
|
||||
return rpcInfo.value
|
||||
}
|
||||
|
||||
// ---------- 内核更新 / 安装 ----------
|
||||
const checkKernelUpdate = async (): Promise<KernelUpdateInfo> => {
|
||||
return await invoke<KernelUpdateInfo>('downloader_check_kernel_update')
|
||||
}
|
||||
|
||||
const updateKernel = async (mirrorPrefix: string = '') => {
|
||||
await invoke('downloader_update_kernel', { mirrorPrefix })
|
||||
await refreshKernel()
|
||||
}
|
||||
|
||||
/**
|
||||
* 首次安装内核:调用后端 install_kernel,监听 downloader-kernel-install-progress 事件更新进度
|
||||
* @param mirrorPrefix 镜像源前缀(空串=GitHub 直连)
|
||||
*/
|
||||
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: '准备开始下载...'
|
||||
// ===== 状态 =====
|
||||
const refreshStatus = async () => {
|
||||
try {
|
||||
status.value = await invoke<DownloaderStatus>('downloader_status')
|
||||
} catch (e) {
|
||||
logger.error('获取状态失败: ' + e)
|
||||
}
|
||||
return status.value
|
||||
}
|
||||
|
||||
// ===== 扩展信息 =====
|
||||
const loadExtensionInfo = async () => {
|
||||
extensionInfo.value = await invoke<ExtensionInfo>('downloader_get_extension_info')
|
||||
return extensionInfo.value
|
||||
}
|
||||
|
||||
// ===== 事件监听 =====
|
||||
const startEventListeners = async () => {
|
||||
if (progressUnlisten && completeUnlisten && addedUnlisten) return
|
||||
if (!progressUnlisten) {
|
||||
progressUnlisten = await listen<InstallProgress>('downloader-kernel-install-progress', (e) => {
|
||||
installProgress.value = e.payload
|
||||
progressUnlisten = await listen<ProgressPayload>('download-progress', (e) => {
|
||||
updateTaskProgress(e.payload)
|
||||
})
|
||||
}
|
||||
try {
|
||||
await invoke('downloader_install_kernel', { mirrorPrefix })
|
||||
await refreshKernel()
|
||||
} catch (e) {
|
||||
logger.error('内核安装失败: ' + e)
|
||||
throw e
|
||||
} finally {
|
||||
installing.value = false
|
||||
if (progressUnlisten) {
|
||||
progressUnlisten()
|
||||
progressUnlisten = null
|
||||
}
|
||||
if (!completeUnlisten) {
|
||||
completeUnlisten = await listen<CompletePayload>('download-complete', (e) => {
|
||||
handleTaskComplete(e.payload)
|
||||
})
|
||||
}
|
||||
if (!addedUnlisten) {
|
||||
addedUnlisten = await listen<{ id: string }>('download-added', () => {
|
||||
// 新任务加入(可能是扩展通过 HTTP API 添加的),刷新任务列表
|
||||
refreshTasks()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const clearInstallProgress = () => {
|
||||
installProgress.value = null
|
||||
const stopEventListeners = () => {
|
||||
if (progressUnlisten) {
|
||||
progressUnlisten()
|
||||
progressUnlisten = null
|
||||
}
|
||||
if (completeUnlisten) {
|
||||
completeUnlisten()
|
||||
completeUnlisten = null
|
||||
}
|
||||
if (addedUnlisten) {
|
||||
addedUnlisten()
|
||||
addedUnlisten = null
|
||||
}
|
||||
}
|
||||
|
||||
/** 用系统资源管理器打开目录(Rust 端绕过 opener scope 限制) */
|
||||
// ===== 初始化 =====
|
||||
const init = async () => {
|
||||
await Promise.all([refreshStatus(), loadSettings(), loadExtensionInfo(), refreshTasks()])
|
||||
await startEventListeners()
|
||||
}
|
||||
|
||||
// ===== 工具函数 =====
|
||||
const openDir = (path: string) => invoke<void>('downloader_open_dir', { path })
|
||||
|
||||
return {
|
||||
// state
|
||||
kernel,
|
||||
tasks,
|
||||
status,
|
||||
version,
|
||||
settings,
|
||||
rpcInfo,
|
||||
activeTasks,
|
||||
waitingTasks,
|
||||
stoppedTasks,
|
||||
globalStat,
|
||||
installing,
|
||||
installProgress,
|
||||
// kernel & process
|
||||
refreshKernel,
|
||||
refreshStatus,
|
||||
start,
|
||||
stop,
|
||||
restart,
|
||||
waitForApi,
|
||||
refreshVersion,
|
||||
extensionInfo,
|
||||
// tasks
|
||||
refreshActive,
|
||||
refreshWaiting,
|
||||
refreshStopped,
|
||||
refreshAllTasks,
|
||||
refreshGlobalStat,
|
||||
addUri,
|
||||
refreshTasks,
|
||||
addTask,
|
||||
pauseTask,
|
||||
unpauseTask,
|
||||
resumeTask,
|
||||
removeTask,
|
||||
changeGlobalOption,
|
||||
// settings
|
||||
loadSettings,
|
||||
saveSettings,
|
||||
// rpc info
|
||||
loadRpcInfo,
|
||||
// kernel update / install
|
||||
checkKernelUpdate,
|
||||
updateKernel,
|
||||
installKernel,
|
||||
clearInstallProgress,
|
||||
openDir,
|
||||
loadHistory,
|
||||
persistHistory
|
||||
// status
|
||||
refreshStatus,
|
||||
// extension
|
||||
loadExtensionInfo,
|
||||
// events
|
||||
startEventListeners,
|
||||
stopEventListeners,
|
||||
// init
|
||||
init,
|
||||
// utils
|
||||
openDir
|
||||
}
|
||||
})
|
||||
|
||||
@@ -98,6 +98,9 @@ export const useProxyStore = defineStore('proxy', () => {
|
||||
const settings = ref<ProxySettings | null>(null)
|
||||
const systemProxy = ref(false)
|
||||
|
||||
/** 是否已完成首次加载(避免初始 null/false 导致闪烁误导状态) */
|
||||
const initialized = ref(false)
|
||||
|
||||
// ===== 内核安装进度 =====
|
||||
const installing = ref(false)
|
||||
const installProgress = ref<InstallProgress | null>(null)
|
||||
@@ -325,6 +328,7 @@ export const useProxyStore = defineStore('proxy', () => {
|
||||
proxies,
|
||||
settings,
|
||||
systemProxy,
|
||||
initialized,
|
||||
installing,
|
||||
installProgress,
|
||||
// kernel & process
|
||||
|
||||
Reference in New Issue
Block a user