下载模块 Init
This commit is contained in:
@@ -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
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user