下载模块 Init

This commit is contained in:
zhongluofeng
2026-07-20 18:28:34 +08:00
parent e84958e0fc
commit 548b022426
37 changed files with 4795 additions and 65 deletions
+403
View File
@@ -0,0 +1,403 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { invoke } from '@tauri-apps/api/core'
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
import { createLogger } from '@/lib/logger'
const logger = createLogger('downloader')
// ===== 与 Rust 端对应的数据结构(camelCase =====
export interface DownloaderSettings {
rpcPort: number
rpcSecret: string
downloadDir: string
maxConcurrent: number
maxConnectionPerServer: number
split: 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
}
export interface DownloaderStatus {
running: boolean
pid: number | null
restartCount: number
}
export interface RpcInfo {
rpcUrl: string
rpcSecret: string
extensionPath: string | null
}
/** aria2 任务文件信息 */
export interface Aria2File {
path: string
length: number
completedLength: number
selected: boolean
}
/** 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[]
}
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 settings = ref<DownloaderSettings | null>(null)
const rpcInfo = ref<RpcInfo | 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
/** 内核信息(同时尝试从 resource 提取到 cores/ */
const refreshKernel = async () => {
try {
kernel.value = await invoke<KernelInfo>('downloader_kernel_info')
} catch (e) {
logger.error('获取内核信息失败: ' + e)
}
return kernel.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 refreshActive = async () => {
try {
const res = await invoke<Aria2Task[]>('downloader_get_active')
activeTasks.value = res ?? []
} catch (e) {
logger.error('获取活跃任务失败: ' + e)
}
return activeTasks.value
}
const refreshWaiting = async () => {
try {
const res = await invoke<Aria2Task[]>('downloader_get_waiting')
waitingTasks.value = res ?? []
} catch (e) {
logger.error('获取等待任务失败: ' + e)
}
return waitingTasks.value
}
const refreshStopped = async () => {
try {
const res = await invoke<Aria2Task[]>('downloader_get_stopped')
stoppedTasks.value = res ?? []
} catch (e) {
logger.error('获取已完成任务失败: ' + e)
}
return stoppedTasks.value
}
/** 刷新全部任务(active + waiting + stopped */
const refreshAllTasks = async () => {
await Promise.all([refreshActive(), refreshWaiting(), refreshStopped()])
// 刷新后持久化历史快照
persistHistory()
}
const refreshGlobalStat = async () => {
try {
globalStat.value = await invoke<GlobalStat>('downloader_get_global_stat')
} catch (e) {
logger.error('获取全局统计失败: ' + e)
}
return globalStat.value
}
// ---------- 任务操作 ----------
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
}
const saveSettings = async (s: DownloaderSettings) => {
await invoke('downloader_save_settings', { settings: s })
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: '准备开始下载...'
}
if (!progressUnlisten) {
progressUnlisten = await listen<InstallProgress>('downloader-kernel-install-progress', (e) => {
installProgress.value = 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
}
}
}
const clearInstallProgress = () => {
installProgress.value = null
}
/** 用系统资源管理器打开目录(Rust 端绕过 opener scope 限制) */
const openDir = (path: string) => invoke<void>('downloader_open_dir', { path })
return {
// state
kernel,
status,
version,
settings,
rpcInfo,
activeTasks,
waitingTasks,
stoppedTasks,
globalStat,
installing,
installProgress,
// kernel & process
refreshKernel,
refreshStatus,
start,
stop,
restart,
waitForApi,
refreshVersion,
// tasks
refreshActive,
refreshWaiting,
refreshStopped,
refreshAllTasks,
refreshGlobalStat,
addUri,
pauseTask,
unpauseTask,
removeTask,
changeGlobalOption,
// settings
loadSettings,
saveSettings,
// rpc info
loadRpcInfo,
// kernel update / install
checkKernelUpdate,
updateKernel,
installKernel,
clearInstallProgress,
openDir,
loadHistory,
persistHistory
}
})
+75
View File
@@ -0,0 +1,75 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
/**
* 模块标签栏跨组件状态。
*
* 用于在模块详情页的 TabsList 因滚动消失时,
* 在顶部 TitleBar 中显示一组浮动切换按钮。
*
* 工作流程:
* 1. 模块(如 DownloaderModuleonMounted 时调用 registerTabs(),传入标签定义和当前值
* 2. 模块用 IntersectionObserver 或 scroll 监听检测 TabsList 可见性,调用 setFloatingVisible()
* 3. 模块通过 watch 将本地 activeTab 同步到 store
* 4. TitleBar 读取 store 的 tabs / activeTab / floatingVisible 渲染浮动切换器
* 5. 用户点击浮动切换器时调用 setActiveTab(),模块监听 store.activeTab 变化更新本地值
* 6. 模块 onUnmounted 时调用 unregisterTabs() 清理状态
*/
export interface ModuleTab {
value: string
label: string
}
export const useModuleTabsStore = defineStore('moduleTabs', () => {
/** 当前模块注册的标签列表(空表示无模块注册,TitleBar 不渲染) */
const tabs = ref<ModuleTab[]>([])
/** 当前活跃标签值 */
const activeTab = ref<string>('')
/** 是否显示浮动切换器(TabsList 滚出可视区时为 true */
const floatingVisible = ref<boolean>(false)
/** 模块注册标签(onMounted 时调用) */
const registerTabs = (tabList: ModuleTab[], current: string) => {
tabs.value = tabList
activeTab.value = current
floatingVisible.value = false
}
/** 模块注销标签(onUnmounted 时调用) */
const unregisterTabs = () => {
tabs.value = []
activeTab.value = ''
floatingVisible.value = false
}
/** 设置浮动切换器可见性 */
const setFloatingVisible = (visible: boolean) => {
// 仅在有注册标签时才允许显示
if (!visible) {
floatingVisible.value = false
return
}
if (tabs.value.length > 0) {
floatingVisible.value = true
}
}
/**
* 设置活跃标签(双向同步用)。
* - 模块本地 activeTab 变化时调用此方法同步到 store
* - TitleBar 点击时也调用此方法,模块通过 watch 感知变化
*/
const setActiveTab = (value: string) => {
activeTab.value = value
}
return {
tabs,
activeTab,
floatingVisible,
registerTabs,
unregisterTabs,
setFloatingVisible,
setActiveTab
}
})