289 lines
9.9 KiB
TypeScript
289 lines
9.9 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { invoke } from '@tauri-apps/api/core'
|
|
import { listen } from '@tauri-apps/api/event'
|
|
|
|
const sortByName = (a: any, b: any) => a.name.localeCompare(b.name, 'zh-CN', { sensitivity: 'accent' });
|
|
|
|
// 版本比对工具函数
|
|
const compareVersions = (v1: string | null | undefined, v2: string | null | undefined) => {
|
|
if (!v1 || !v2) return 0;
|
|
if (v1 === v2) return 0;
|
|
const cleanV = (v: string) => v.replace(/^v/i, '').split(/[-+]/)[0].split('.');
|
|
const p1 = cleanV(v1);
|
|
const p2 = cleanV(v2);
|
|
const len = Math.max(p1.length, p2.length);
|
|
for (let i = 0; i < len; i++) {
|
|
const n1 = parseInt(p1[i] || '0', 10);
|
|
const n2 = parseInt(p2[i] || '0', 10);
|
|
if (n1 < n2) return -1;
|
|
if (n1 > n2) return 1;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
export interface LogEntry {
|
|
id: string; // 日志唯一标识
|
|
timestamp: string;
|
|
command: string;
|
|
output: string;
|
|
status: 'info' | 'success' | 'error';
|
|
}
|
|
|
|
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<string, { status: string, progress: number, targetVersion?: string }>,
|
|
loading: false,
|
|
isInitialized: false,
|
|
initStatus: '正在检查系统环境...',
|
|
lastFetched: 0
|
|
}),
|
|
getters: {
|
|
mergedEssentials: (state) => {
|
|
return state.essentials.map(item => {
|
|
const installedInfo = state.allSoftware.find(s => s.id.toLowerCase() === item.id.toLowerCase());
|
|
const wingetUpdate = state.updates.find(s => s.id.toLowerCase() === item.id.toLowerCase());
|
|
|
|
const task = state.activeTasks[item.id];
|
|
const isInstalled = !!installedInfo;
|
|
const currentVersion = installedInfo?.version;
|
|
const recommendedVersion = item.version; // 清单里的推荐版本
|
|
const availableVersion = wingetUpdate?.available_version; // Winget 查到的最新版
|
|
|
|
let displayStatus = task ? task.status : 'idle';
|
|
let actionLabel = '安装';
|
|
let targetVersion = recommendedVersion || availableVersion;
|
|
|
|
if (isInstalled) {
|
|
// 逻辑:已安装 >= 推荐 -> 已安装(禁用)
|
|
// 逻辑:已安装 < 推荐 -> 更新
|
|
const comp = compareVersions(currentVersion, recommendedVersion);
|
|
if (comp >= 0) {
|
|
displayStatus = task ? task.status : 'installed';
|
|
actionLabel = '已安装';
|
|
targetVersion = undefined; // 禁用安装
|
|
} else {
|
|
actionLabel = '更新';
|
|
targetVersion = recommendedVersion;
|
|
}
|
|
} else {
|
|
actionLabel = '安装';
|
|
targetVersion = recommendedVersion || availableVersion;
|
|
}
|
|
|
|
return {
|
|
...item,
|
|
version: currentVersion,
|
|
recommended_version: recommendedVersion,
|
|
available_version: availableVersion,
|
|
status: displayStatus,
|
|
progress: task ? task.progress : 0,
|
|
actionLabel,
|
|
targetVersion // 传递给视图,用于点击安装
|
|
};
|
|
});
|
|
},
|
|
sortedUpdates: (state) => {
|
|
return [...state.updates].map(item => {
|
|
const task = state.activeTasks[item.id];
|
|
return {
|
|
...item,
|
|
status: task ? task.status : 'idle',
|
|
progress: task ? task.progress : 0,
|
|
actionLabel: '更新',
|
|
targetVersion: item.available_version // 更新页面永远追求最新版
|
|
};
|
|
}).sort(sortByName);
|
|
},
|
|
isBusy: (state) => {
|
|
return state.loading || Object.values(state.activeTasks).some(task =>
|
|
task.status === 'pending' || task.status === 'installing'
|
|
);
|
|
}
|
|
},
|
|
actions: {
|
|
// ... (initializeApp, saveSettings, syncEssentials stay the same)
|
|
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;
|
|
},
|
|
|
|
async syncEssentials() {
|
|
this.loading = true;
|
|
try {
|
|
await invoke('sync_essentials');
|
|
await this.fetchEssentials();
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
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));
|
|
}
|
|
},
|
|
|
|
async fetchEssentials() {
|
|
let repo = await invoke('get_essentials') as any;
|
|
if (!repo) {
|
|
try {
|
|
await invoke('sync_essentials');
|
|
repo = await invoke('get_essentials') as any;
|
|
} catch (err) {
|
|
console.error('Initial sync failed:', err);
|
|
}
|
|
}
|
|
if (repo) {
|
|
this.essentials = repo.essentials;
|
|
this.essentialsVersion = repo.version;
|
|
} else {
|
|
this.essentials = [];
|
|
this.essentialsVersion = '';
|
|
}
|
|
},
|
|
async fetchUpdates() {
|
|
if (this.isBusy) return;
|
|
this.loading = true
|
|
try {
|
|
const res = await invoke('get_updates')
|
|
this.updates = res as any[]
|
|
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 {
|
|
await this.fetchEssentials();
|
|
const [all, updates] = await Promise.all([
|
|
invoke('get_installed_software'),
|
|
invoke('get_updates')
|
|
]);
|
|
this.allSoftware = all as any[];
|
|
this.updates = updates as any[];
|
|
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) {
|
|
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
|
|
}
|
|
})
|
|
} catch (err) {
|
|
console.error('Invoke install failed:', err);
|
|
this.activeTasks[id] = { status: 'error', progress: 0 };
|
|
}
|
|
}
|
|
},
|
|
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)
|
|
},
|
|
initListener() {
|
|
if ((window as any).__tauri_listener_init) return;
|
|
(window as any).__tauri_listener_init = true;
|
|
|
|
listen('install-status', (event: any) => {
|
|
const { id, status, progress } = event.payload
|
|
const task = this.activeTasks[id];
|
|
this.activeTasks[id] = { status, progress, targetVersion: task?.targetVersion };
|
|
|
|
if (status === 'success') {
|
|
this.lastFetched = 0;
|
|
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];
|
|
this.fetchAllData();
|
|
}
|
|
}, 3000);
|
|
}
|
|
})
|
|
|
|
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();
|
|
}
|
|
})
|
|
}
|
|
}
|
|
})
|