From 0fc523e234b355f2ed8d0a6b8f4374f4514a1080 Mon Sep 17 00:00:00 2001 From: Julian Freeman Date: Sat, 18 Apr 2026 16:01:38 -0400 Subject: [PATCH] refactor 3 --- src/store/catalog.ts | 130 +++++++++++ src/store/software.ts | 452 +++++++++++++-------------------------- src/store/taskRuntime.ts | 144 +++++++++++++ src/store/types.ts | 52 +++++ 4 files changed, 471 insertions(+), 307 deletions(-) create mode 100644 src/store/catalog.ts create mode 100644 src/store/taskRuntime.ts create mode 100644 src/store/types.ts diff --git a/src/store/catalog.ts b/src/store/catalog.ts new file mode 100644 index 0000000..1877eb0 --- /dev/null +++ b/src/store/catalog.ts @@ -0,0 +1,130 @@ +import { defineStore } from 'pinia' +import { invoke } from '@tauri-apps/api/core' + +import type { AppSettings, DashboardSnapshot, SoftwareListItem, SyncEssentialsResult, UpdateCandidate } from './types' + +type EssentialsStatusResponse = [string, SoftwareListItem[]] + +export const useCatalogStore = defineStore('catalog', { + state: () => ({ + essentials: [] as SoftwareListItem[], + essentialsVersion: '', + updates: [] as UpdateCandidate[], + allSoftware: [] as SoftwareListItem[], + settings: { + repo_url: 'https://karlblue.github.io/winget-repo' + } as AppSettings, + loading: false, + isInitialized: false, + initStatus: '正在检查系统环境...', + lastFetched: 0 + }), + actions: { + async initializeApp() { + if (this.isInitialized) return + this.initStatus = '正在加载应用配置...' + try { + this.settings = await invoke('get_settings') + this.initStatus = '正在同步 Winget 模块...' + await invoke('initialize_app') + this.isInitialized = true + } catch { + this.initStatus = '环境配置失败,请检查运行日志' + setTimeout(() => { this.isInitialized = true }, 2000) + } + }, + + async saveSettings(newSettings: AppSettings) { + await invoke('save_settings', { settings: newSettings }) + this.settings = newSettings + }, + + async syncEssentials() { + this.loading = true + try { + const result = await invoke('sync_essentials') as SyncEssentialsResult + await this.fetchEssentials() + return result + } finally { + this.loading = false + } + }, + + async fetchEssentials() { + let response = await invoke('get_essentials_status') as EssentialsStatusResponse + if ((!response || !response[1]) && !(await invoke('get_essentials') as unknown)) { + try { + await invoke('sync_essentials') + response = await invoke('get_essentials_status') as EssentialsStatusResponse + } catch (err) { + console.error('Initial sync failed:', err) + } + } + if (response && Array.isArray(response[1])) { + this.essentialsVersion = response[0] || '' + this.essentials = response[1] + } else { + this.essentials = [] + this.essentialsVersion = '' + } + }, + + async fetchUpdates() { + this.loading = true + try { + const res = await invoke('get_update_candidates') + this.updates = res as UpdateCandidate[] + await this.loadIconsForUpdates() + } finally { + this.loading = false + } + }, + + async syncDataIfNeeded(force = false) { + const now = Date.now() + const cacheTimeout = 5 * 60 * 1000 + if (!force && this.allSoftware.length > 0 && (now - this.lastFetched < cacheTimeout)) { + if (this.essentials.length === 0) await this.fetchEssentials() + return + } + await this.fetchAllData() + }, + + async fetchAllData() { + this.loading = true + try { + const snapshot = await invoke('get_dashboard_snapshot') as DashboardSnapshot + this.applyDashboardSnapshot(snapshot) + await this.loadIconsForUpdates() + this.lastFetched = Date.now() + } finally { + this.loading = false + } + }, + + applyDashboardSnapshot(snapshot: DashboardSnapshot) { + this.essentialsVersion = snapshot.essentials_version + this.essentials = snapshot.essentials + this.updates = snapshot.updates + this.allSoftware = snapshot.installed_software + }, + + async loadIconsForUpdates() { + const targets = this.updates.filter(item => !item.icon_url && item.id && item.name) + await Promise.allSettled(targets.map(async (item) => { + const iconUrl = await invoke('get_software_icon', { id: item.id, name: item.name }) as string | null + if (!iconUrl) return + const target = this.updates.find(update => update.id === item.id) + if (target) { + target.icon_url = iconUrl + } + })) + }, + + findSoftware(id: string) { + return this.essentials.find(s => s.id === id) + || this.updates.find(s => s.id === id) + || this.allSoftware.find(s => s.id === id) + } + } +}) diff --git a/src/store/software.ts b/src/store/software.ts index 3756c4b..a649e01 100644 --- a/src/store/software.ts +++ b/src/store/software.ts @@ -1,322 +1,160 @@ -import { defineStore } from 'pinia' -import { invoke } from '@tauri-apps/api/core' -import { listen } from '@tauri-apps/api/event' +import { defineStore, storeToRefs } from 'pinia' +import { computed } from 'vue' -export interface LogEntry { - id: string; // 日志唯一标识 - timestamp: string; - command: string; - output: string; - status: 'info' | 'success' | 'error'; -} +import { useCatalogStore } from './catalog' +import { useTaskRuntimeStore } from './taskRuntime' -interface SyncEssentialsResult { - status: 'updated' | 'cache_used'; - message: string; -} +export const useSoftwareStore = defineStore('software', () => { + const catalog = useCatalogStore() + const taskRuntime = useTaskRuntimeStore() -interface DashboardSnapshot { - essentials_version: string; - essentials: any[]; - updates: any[]; - installed_software: any[]; -} + const { + essentials, + essentialsVersion, + updates, + allSoftware, + settings, + loading, + isInitialized, + initStatus + } = storeToRefs(catalog) -interface EssentialsStatusResponse extends Array { - 0: string; - 1: any[]; -} + const { + activeTasks, + selectedEssentialIds, + selectedUpdateIds, + logs, + postInstallPrefs + } = storeToRefs(taskRuntime) -export const useSoftwareStore = defineStore('software', { - state: () => ({ - essentials: [] as any[], - essentialsVersion: '', - updates: [] as any[], - allSoftware: [] as any[], - selectedEssentialIds: [] as string[], - selectedUpdateIds: [] as string[], - logs: [] as LogEntry[], - settings: { - repo_url: 'https://karlblue.github.io/winget-repo' - }, - activeTasks: {} as Record, - loading: false, - isInitialized: false, - initStatus: '正在检查系统环境...', - lastFetched: 0, - refreshTimer: null as any, - batchQueue: [] as string[], - postInstallPrefs: {} as Record // 记录用户对每个软件后安装配置的偏好 - }), - getters: { - mergedEssentials: (state) => { - return state.essentials.map(item => { - const task = state.activeTasks[item.id]; - const enablePostInstall = state.postInstallPrefs[item.id] !== false; - const baseStatus = item.actionLabel === '已安装' ? 'installed' : 'idle'; + const mergedEssentials = computed(() => essentials.value.map(item => { + const task = activeTasks.value[item.id] + const enablePostInstall = postInstallPrefs.value[item.id] !== false + const baseStatus = item.action_label === '已安装' ? 'installed' : 'idle' - return { - ...item, - status: task ? task.status : baseStatus, - progress: task ? task.progress : 0, - enablePostInstall - }; - }); - }, - sortedUpdates: (state) => { - return [...state.updates].map(item => { - const task = state.activeTasks[item.id]; - const enablePostInstall = state.postInstallPrefs[item.id] !== false; - return { - ...item, - status: task ? task.status : 'idle', - progress: task ? task.progress : 0, - enablePostInstall - }; - }).sort((a, b) => a.name.localeCompare(b.name, 'zh-CN', { sensitivity: 'accent' })); - }, - isBusy: (state) => { - return state.loading || Object.values(state.activeTasks).some(task => - task.status === 'pending' || task.status === 'installing' - ); + return { + ...item, + version: item.version ?? undefined, + recommended_version: item.recommended_version ?? undefined, + available_version: item.available_version ?? undefined, + icon_url: item.icon_url ?? undefined, + manifest_url: item.manifest_url ?? undefined, + post_install_url: item.post_install_url ?? undefined, + actionLabel: item.action_label, + targetVersion: item.target_version ?? undefined, + status: task ? task.status : baseStatus, + progress: task ? task.progress : 0, + enablePostInstall } - }, - actions: { - async initializeApp() { - if (this.isInitialized) return; - this.initStatus = '正在加载应用配置...'; - try { - this.settings = await invoke('get_settings'); - this.initStatus = '正在同步 Winget 模块...'; - await invoke('initialize_app'); - this.isInitialized = true; - } catch (err) { - this.initStatus = '环境配置失败,请检查运行日志'; - setTimeout(() => { this.isInitialized = true; }, 2000); - } - }, + })) - async saveSettings(newSettings: any) { - await invoke('save_settings', { settings: newSettings }); - this.settings = newSettings; - }, + const sortedUpdates = computed(() => [...updates.value].map(item => { + const task = activeTasks.value[item.id] + const enablePostInstall = postInstallPrefs.value[item.id] !== false - async syncEssentials() { - this.loading = true; - try { - const result = await invoke('sync_essentials') as SyncEssentialsResult; - await this.fetchEssentials(); - return result; - } finally { - this.loading = false; - } - }, + return { + ...item, + version: item.version ?? undefined, + recommended_version: item.recommended_version ?? undefined, + available_version: item.available_version ?? undefined, + icon_url: item.icon_url ?? undefined, + manifest_url: item.manifest_url ?? undefined, + post_install_url: item.post_install_url ?? undefined, + actionLabel: item.action_label, + targetVersion: item.target_version ?? undefined, + status: task ? task.status : 'idle', + progress: task ? task.progress : 0, + enablePostInstall + } + }).sort((a, b) => a.name.localeCompare(b.name, 'zh-CN', { sensitivity: 'accent' }))) - toggleSelection(id: string, type: 'essential' | 'update') { - if (this.isBusy) return; - const list = type === 'essential' ? this.selectedEssentialIds : this.selectedUpdateIds; - const index = list.indexOf(id); - if (index === -1) list.push(id); - else list.splice(index, 1); - }, - selectAll(type: 'essential' | 'update') { - if (type === 'essential') { - const selectable = this.mergedEssentials.filter(s => s.actionLabel !== '已安装'); - this.selectedEssentialIds = selectable.map(s => s.id); - } else { - this.selectedUpdateIds = this.updates.map(s => s.id); - } - }, - deselectAll(type: 'essential' | 'update') { - if (type === 'essential') this.selectedEssentialIds = []; - else this.selectedUpdateIds = []; - }, - invertSelection(type: 'essential' | 'update') { - if (type === 'essential') { - const selectable = this.mergedEssentials.filter(s => s.actionLabel !== '已安装').map(s => s.id); - this.selectedEssentialIds = selectable.filter(id => !this.selectedEssentialIds.includes(id)); - } else { - const selectable = this.updates.map(s => s.id); - this.selectedUpdateIds = selectable.filter(id => !this.selectedUpdateIds.includes(id)); - } - }, + const isBusy = computed(() => loading.value || taskRuntime.isTaskBusy) - async fetchEssentials() { - let response = await invoke('get_essentials_status') as EssentialsStatusResponse; - if ((!response || !response[1]) && !(await invoke('get_essentials') as any)) { - try { - await invoke('sync_essentials'); - response = await invoke('get_essentials_status') as EssentialsStatusResponse; - } catch (err) { - console.error('Initial sync failed:', err); - } - } - if (response && Array.isArray(response[1])) { - this.essentialsVersion = response[0] || ''; - this.essentials = response[1]; - } else { - this.essentials = []; - this.essentialsVersion = ''; - } - }, - async fetchUpdates() { - if (this.isBusy) return; - this.loading = true - try { - const res = await invoke('get_update_candidates') - this.updates = res as any[] - await this.loadIconsForUpdates() - if (this.selectedUpdateIds.length === 0) this.selectAll('update'); - } finally { - this.loading = false - } - }, - async syncDataIfNeeded(force = false) { - if (this.isBusy) return; - const now = Date.now(); - const CACHE_TIMEOUT = 5 * 60 * 1000; - if (!force && this.allSoftware.length > 0 && (now - this.lastFetched < CACHE_TIMEOUT)) { - if (this.essentials.length === 0) await this.fetchEssentials(); - return; - } - await this.fetchAllData(); - }, - async fetchAllData() { - this.loading = true; - try { - const snapshot = await invoke('get_dashboard_snapshot') as DashboardSnapshot; - this.essentialsVersion = snapshot.essentials_version; - this.essentials = snapshot.essentials; - this.allSoftware = snapshot.installed_software; - this.updates = snapshot.updates; - await this.loadIconsForUpdates() - this.lastFetched = Date.now(); - if (this.selectedEssentialIds.length === 0) this.selectAll('essential'); - } finally { - this.loading = false; - } - }, - async install(id: string, targetVersion?: string) { - const software = this.findSoftware(id) - if (software) { - // 根据偏好决定是否开启后安装配置 - const enablePostInstall = this.postInstallPrefs[id] !== false; - - this.activeTasks[id] = { status: 'pending', progress: 0, targetVersion }; - try { - await invoke('install_software', { - task: { - id, - version: targetVersion, - use_manifest: software.use_manifest || false, - manifest_url: software.manifest_url || null, - enable_post_install: enablePostInstall - } - }) - } catch (err) { - console.error('Invoke install failed:', err); - this.activeTasks[id] = { status: 'error', progress: 0 }; - } - } - }, + const toggleSelection = (id: string, type: 'essential' | 'update') => { + if (isBusy.value) return + taskRuntime.toggleSelection(id, type) + } - togglePostInstallPref(id: string) { - const current = this.postInstallPrefs[id] !== false; - this.postInstallPrefs[id] = !current; - }, - - startBatch(ids: string[]) { - this.batchQueue = [...ids]; - }, - - scheduleDataRefresh() { - if (this.refreshTimer) clearTimeout(this.refreshTimer); - - this.refreshTimer = setTimeout(async () => { - await this.fetchAllData(); - Object.keys(this.activeTasks).forEach(id => { - const status = this.activeTasks[id].status; - if (status === 'success' || status === 'error') { - delete this.activeTasks[id]; - } - }); - this.refreshTimer = null; - }, 2000); - }, - - findSoftware(id: string) { - return this.essentials.find(s => s.id === id) || - this.updates.find(s => s.id === id) || - this.allSoftware.find(s => s.id === id) - }, - async loadIconsForUpdates() { - const targets = this.updates.filter(item => !item.icon_url && item.id && item.name); - await Promise.allSettled(targets.map(async (item) => { - const iconUrl = await invoke('get_software_icon', { id: item.id, name: item.name }) as string | null; - if (!iconUrl) return; - const target = this.updates.find(update => update.id === item.id); - if (target) { - target.icon_url = iconUrl; - } - })); - }, - initListener() { - if ((window as any).__tauri_listener_init) return; - (window as any).__tauri_listener_init = true; - - listen('install-status', async (event: any) => { - const { id, status, progress } = event.payload - const task = this.activeTasks[id]; - - this.activeTasks[id] = { status, progress, targetVersion: task?.targetVersion }; - - if (status === 'success' || status === 'error') { - if (status === 'success') { - try { - const latestInfo = await invoke('get_software_info', { id }) as any; - if (latestInfo) { - const index = this.allSoftware.findIndex(s => s.id.toLowerCase() === id.toLowerCase()); - if (index !== -1) { - this.allSoftware[index] = { ...this.allSoftware[index], ...latestInfo }; - } else { - this.allSoftware.push(latestInfo); - } - } - } catch (err) { - console.error('Partial refresh failed:', err); - } - - this.selectedEssentialIds = this.selectedEssentialIds.filter(i => i !== id); - this.selectedUpdateIds = this.selectedUpdateIds.filter(i => i !== id); - - setTimeout(() => { - if (this.activeTasks[id]?.status === 'success') { - delete this.activeTasks[id]; - } - }, 3000); - } - - const index = this.batchQueue.indexOf(id); - if (index !== -1) { - this.batchQueue.splice(index, 1); - if (this.batchQueue.length === 0) { - this.scheduleDataRefresh(); - } - } - } - }) - - listen('log-event', (event: any) => { - const payload = event.payload as LogEntry; - const existingLog = this.logs.find(l => l.id === payload.id); - if (existingLog) { - if (payload.output) existingLog.output += '\n' + payload.output; - if (payload.status !== 'info') existingLog.status = payload.status; - } else { - this.logs.unshift(payload); - if (this.logs.length > 100) this.logs.pop(); - } - }) + const selectAll = (type: 'essential' | 'update') => { + if (type === 'essential') { + const selectable = mergedEssentials.value.filter(item => item.actionLabel !== '已安装') + taskRuntime.setSelection('essential', selectable.map(item => item.id)) + } else { + taskRuntime.setSelection('update', updates.value.map(item => item.id)) } } + + const deselectAll = (type: 'essential' | 'update') => { + taskRuntime.setSelection(type, []) + } + + const invertSelection = (type: 'essential' | 'update') => { + if (type === 'essential') { + const selectable = mergedEssentials.value + .filter(item => item.actionLabel !== '已安装') + .map(item => item.id) + taskRuntime.setSelection( + 'essential', + selectable.filter(id => !selectedEssentialIds.value.includes(id)) + ) + } else { + const selectable = updates.value.map(item => item.id) + taskRuntime.setSelection( + 'update', + selectable.filter(id => !selectedUpdateIds.value.includes(id)) + ) + } + } + + const fetchUpdates = async () => { + if (isBusy.value) return + await catalog.fetchUpdates() + if (selectedUpdateIds.value.length === 0) selectAll('update') + } + + const syncDataIfNeeded = async (force = false) => { + if (isBusy.value) return + await catalog.syncDataIfNeeded(force) + if (selectedEssentialIds.value.length === 0) selectAll('essential') + } + + const fetchAllData = async () => { + await catalog.fetchAllData() + if (selectedEssentialIds.value.length === 0) selectAll('essential') + } + + return { + essentials, + essentialsVersion, + updates, + allSoftware, + selectedEssentialIds, + selectedUpdateIds, + logs, + settings, + activeTasks, + loading, + isInitialized, + initStatus, + mergedEssentials, + sortedUpdates, + isBusy, + initializeApp: catalog.initializeApp, + saveSettings: catalog.saveSettings, + syncEssentials: catalog.syncEssentials, + fetchEssentials: catalog.fetchEssentials, + fetchUpdates, + syncDataIfNeeded, + fetchAllData, + install: taskRuntime.install, + togglePostInstallPref: taskRuntime.togglePostInstallPref, + startBatch: taskRuntime.startBatch, + scheduleDataRefresh: taskRuntime.scheduleDataRefresh, + findSoftware: catalog.findSoftware, + initListener: taskRuntime.initListener, + toggleSelection, + selectAll, + deselectAll, + invertSelection + } }) diff --git a/src/store/taskRuntime.ts b/src/store/taskRuntime.ts new file mode 100644 index 0000000..0dbb37c --- /dev/null +++ b/src/store/taskRuntime.ts @@ -0,0 +1,144 @@ +import { defineStore } from 'pinia' +import { invoke } from '@tauri-apps/api/core' +import { listen } from '@tauri-apps/api/event' + +import { useCatalogStore } from './catalog' +import type { ActiveTaskState, LogEntry } from './types' + +export const useTaskRuntimeStore = defineStore('task-runtime', { + state: () => ({ + activeTasks: {} as Record, + selectedEssentialIds: [] as string[], + selectedUpdateIds: [] as string[], + logs: [] as LogEntry[], + refreshTimer: null as ReturnType | null, + batchQueue: [] as string[], + postInstallPrefs: {} as Record + }), + getters: { + isTaskBusy: (state) => Object.values(state.activeTasks).some(task => + task.status === 'pending' || task.status === 'installing' + ) + }, + actions: { + toggleSelection(id: string, type: 'essential' | 'update') { + const list = type === 'essential' ? this.selectedEssentialIds : this.selectedUpdateIds + const index = list.indexOf(id) + if (index === -1) list.push(id) + else list.splice(index, 1) + }, + + setSelection(type: 'essential' | 'update', ids: string[]) { + if (type === 'essential') this.selectedEssentialIds = ids + else this.selectedUpdateIds = ids + }, + + togglePostInstallPref(id: string) { + const current = this.postInstallPrefs[id] !== false + this.postInstallPrefs[id] = !current + }, + + startBatch(ids: string[]) { + this.batchQueue = [...ids] + }, + + async install(id: string, targetVersion?: string) { + const catalog = useCatalogStore() + const software = catalog.findSoftware(id) + if (!software) return + + const enablePostInstall = this.postInstallPrefs[id] !== false + this.activeTasks[id] = { status: 'pending', progress: 0, targetVersion } + try { + await invoke('install_software', { + task: { + id, + version: targetVersion, + use_manifest: software.use_manifest || false, + manifest_url: software.manifest_url || null, + enable_post_install: enablePostInstall + } + }) + } catch (err) { + console.error('Invoke install failed:', err) + this.activeTasks[id] = { status: 'error', progress: 0 } + } + }, + + scheduleDataRefresh() { + const catalog = useCatalogStore() + if (this.refreshTimer) clearTimeout(this.refreshTimer) + + this.refreshTimer = setTimeout(async () => { + await catalog.fetchAllData() + Object.keys(this.activeTasks).forEach(id => { + const status = this.activeTasks[id].status + if (status === 'success' || status === 'error') { + delete this.activeTasks[id] + } + }) + this.refreshTimer = null + }, 2000) + }, + + initListener() { + if ((window as Window & { __tauri_listener_init?: boolean }).__tauri_listener_init) return + ;(window as Window & { __tauri_listener_init?: boolean }).__tauri_listener_init = true + + listen('install-status', async (event: { payload: { id: string, status: string, progress: number } }) => { + const catalog = useCatalogStore() + const { id, status, progress } = event.payload + const task = this.activeTasks[id] + + this.activeTasks[id] = { status, progress, targetVersion: task?.targetVersion } + + if (status === 'success' || status === 'error') { + if (status === 'success') { + try { + const latestInfo = await invoke('get_software_info', { id }) as Record | null + if (latestInfo) { + const index = catalog.allSoftware.findIndex(s => s.id.toLowerCase() === id.toLowerCase()) + if (index !== -1) { + catalog.allSoftware[index] = { ...catalog.allSoftware[index], ...latestInfo } + } else { + catalog.allSoftware.push(latestInfo as never) + } + } + } catch (err) { + console.error('Partial refresh failed:', err) + } + + this.selectedEssentialIds = this.selectedEssentialIds.filter(item => item !== id) + this.selectedUpdateIds = this.selectedUpdateIds.filter(item => item !== id) + + setTimeout(() => { + if (this.activeTasks[id]?.status === 'success') { + delete this.activeTasks[id] + } + }, 3000) + } + + const index = this.batchQueue.indexOf(id) + if (index !== -1) { + this.batchQueue.splice(index, 1) + if (this.batchQueue.length === 0) { + this.scheduleDataRefresh() + } + } + } + }) + + listen('log-event', (event: { payload: LogEntry }) => { + const payload = event.payload + const existingLog = this.logs.find(item => item.id === payload.id) + if (existingLog) { + if (payload.output) existingLog.output += '\n' + payload.output + if (payload.status !== 'info') existingLog.status = payload.status + } else { + this.logs.unshift(payload) + if (this.logs.length > 100) this.logs.pop() + } + }) + } + } +}) diff --git a/src/store/types.ts b/src/store/types.ts new file mode 100644 index 0000000..c09ce2f --- /dev/null +++ b/src/store/types.ts @@ -0,0 +1,52 @@ +export interface LogEntry { + id: string + timestamp: string + command: string + output: string + status: 'info' | 'success' | 'error' +} + +export interface SyncEssentialsResult { + status: 'updated' | 'cache_used' + message: string +} + +export interface DashboardSnapshot { + essentials_version: string + essentials: SoftwareListItem[] + updates: UpdateCandidate[] + installed_software: SoftwareListItem[] +} + +export interface SoftwareListItem { + id: string + name: string + description?: string + version?: string | null + recommended_version?: string | null + available_version?: string | null + icon_url?: string | null + use_manifest?: boolean + manifest_url?: string | null + post_install?: unknown + post_install_url?: string | null + actionLabel?: string + action_label?: string + targetVersion?: string | null + target_version?: string | null +} + +export interface UpdateCandidate extends SoftwareListItem { + action_label: string + target_version?: string | null +} + +export interface ActiveTaskState { + status: string + progress: number + targetVersion?: string +} + +export interface AppSettings { + repo_url: string +}