import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { getCurrentWindow, Effect, EffectState } from '@tauri-apps/api/window' import { enable, isEnabled, disable } from '@tauri-apps/plugin-autostart' import { moduleRegistry } from '@/modules/registry' import { useSearchStore } from '@/stores/searchStore' import { useProcessStore } from '@/stores/processStore' import { toast } from 'vue-sonner' import { createLogger } from '@/lib/logger' import { STORAGE_KEYS } from '@/lib/constants' import type { ModuleCategory } from '@/types/module' const logger = createLogger('app') export type Theme = 'light' | 'dark' | 'system' export type EffectType = 'normal' | 'mica' | 'acrylic' export interface ModuleInfo { id: string name: string icon: string enabled: boolean description: string category: ModuleCategory hasProcess: boolean builtin: boolean } /** localStorage 版本号 —— 结构变更时递增,自动清除旧数据 */ const SETTINGS_VERSION = 4 const STORAGE_KEY = STORAGE_KEYS.appSettings /** 从模块注册表初始化模块元信息 */ const initModulesFromRegistry = (): ModuleInfo[] => { return moduleRegistry.getAllMetas().map(meta => ({ id: meta.id, name: meta.name, icon: meta.icon, enabled: meta.enabled, description: meta.description, category: meta.category, hasProcess: meta.hasProcess, builtin: meta.builtin })) } /** 从注册表初始化模块排序(仅用户模块,按 order 字段排序) */ const initModuleOrder = (): string[] => { return moduleRegistry .getAllMetas() .filter(m => !m.builtin) .sort((a, b) => a.order - b.order) .map(m => m.id) } export const useAppStore = defineStore('app', () => { const theme = ref('system') const effect = ref('mica') const isAutoStart = ref(false) const isInitialized = ref(false) const modules = ref(initModulesFromRegistry()) const moduleOrder = ref(initModuleOrder()) /** * 系统真实的深浅色偏好(不受应用 setTheme 影响)。 * 应用启动时用 matchMedia 初始化(此时未调用 setTheme,结果准确), * 之后仅通过 Tauri onThemeChanged 事件更新(该事件反映系统主题变化,不受 setTheme 影响)。 * 用于"跟随系统"卡片色块等需要反映系统真实状态的 UI。 */ const systemDark = ref(window.matchMedia('(prefers-color-scheme: dark)').matches) /** 正在处理切换的模块 ID 集合(防止重复点击) */ const togglingModules = ref>(new Set()) const loadSettings = async () => { try { const saved = localStorage.getItem(STORAGE_KEY) if (saved) { const settings = JSON.parse(saved) // 版本不匹配,清除旧数据 if (settings.version !== SETTINGS_VERSION) { console.warn('[appStore] Settings version mismatch, clearing old data') localStorage.removeItem(STORAGE_KEY) saveSettings() return } if (settings.theme) theme.value = settings.theme if (settings.effect) effect.value = settings.effect if (settings.modules) { const savedModules = settings.modules as Array<{ id: string; enabled: boolean }> savedModules.forEach(sm => { const m = modules.value.find(mod => mod.id === sm.id) if (m) { m.enabled = sm.enabled } }) } // 恢复模块排序:保留已保存的顺序,追加新增模块到末尾 if (settings.moduleOrder) { const savedOrder = settings.moduleOrder as string[] const allUserIds = moduleRegistry .getAllMetas() .filter(m => !m.builtin) .map(m => m.id) const known = savedOrder.filter(id => allUserIds.includes(id)) const newlyAdded = allUserIds.filter(id => !savedOrder.includes(id)) moduleOrder.value = [...known, ...newlyAdded] } } } catch { console.error('Failed to load settings from localStorage') localStorage.removeItem(STORAGE_KEY) } } const saveSettings = () => { try { const modulesData = modules.value.map(m => ({ id: m.id, enabled: m.enabled })) localStorage.setItem(STORAGE_KEY, JSON.stringify({ version: SETTINGS_VERSION, theme: theme.value, effect: effect.value, isAutoStart: isAutoStart.value, modules: modulesData, moduleOrder: moduleOrder.value })) } catch { console.error('Failed to save settings to localStorage') } } const enabledModules = computed(() => modules.value.filter(m => m.enabled)) const getModule = (id: string) => modules.value.find(m => m.id === id) /** 重新排序模块 */ const reorderModules = (newOrder: string[]) => { moduleOrder.value = newOrder saveSettings() } const toggleModule = async (moduleId: string, enabled?: boolean) => { const moduleIndex = modules.value.findIndex(m => m.id === moduleId) if (moduleIndex === -1) { console.warn(`[toggleModule] Module "${moduleId}" not found`) return false } const moduleInfo = modules.value[moduleIndex] // 内置模块不可禁用 if (moduleInfo.builtin && enabled === false) { toast.warning('内置模块无法禁用') return false } // 防止重复操作 if (togglingModules.value.has(moduleId)) { console.log(`[toggleModule] Module "${moduleId}" is already being toggled`) return false } const targetState = enabled !== undefined ? enabled : !moduleInfo.enabled if (targetState === moduleInfo.enabled) { console.log(`[toggleModule] Module "${moduleId}" is already ${targetState ? 'enabled' : 'disabled'}`) return false } console.log(`[toggleModule] Toggling "${moduleId}" from ${moduleInfo.enabled} to ${targetState}`) togglingModules.value.add(moduleId) try { if (!targetState) { // ===== 禁用模块 ===== // 1. 先更新状态(让开关立即响应) modules.value[moduleIndex] = { ...moduleInfo, enabled: false } saveSettings() // 2. 清理搜索项 try { useSearchStore().unregisterModule(moduleId) } catch (e) { console.error(`[toggleModule] Failed to unregister search items for "${moduleId}":`, e) logger.error(`禁用模块 "${moduleId}" 时清理搜索项失败: ${e}`) } // 3. 停止进程 if (moduleInfo.hasProcess) { try { const processStore = useProcessStore() const status = processStore.getProcessStatus(moduleId) if (status && status.status === 'running') { toast.loading(`正在停止 ${moduleInfo.name} 后台进程...`, { id: `stop-${moduleId}` }) await processStore.stopByModule(moduleId) toast.success(`${moduleInfo.name} 进程已停止`, { id: `stop-${moduleId}` }) } } catch (e) { console.error(`[toggleModule] Failed to stop process for "${moduleId}":`, e) logger.error(`停止模块 "${moduleId}" 进程失败: ${e}`) toast.error(`停止 ${moduleInfo.name} 进程失败`, { id: `stop-${moduleId}` }) } } // 4. 调用生命周期钩子 try { const config = moduleRegistry.getConfig(moduleId) await config?.lifecycle?.onDisable?.() } catch (e) { console.error(`[toggleModule] Module onDisable hook failed for "${moduleId}":`, e) logger.error(`模块 "${moduleId}" onDisable 钩子失败: ${e}`) } // 5. 清理组件缓存,释放内存 moduleRegistry.clearComponentCache(moduleId) logger.info(`已禁用模块: ${moduleInfo.name}`) toast.success(`已禁用 ${moduleInfo.name}`) } else { // ===== 启用模块 ===== // 1. 先更新状态(让开关立即响应) modules.value[moduleIndex] = { ...moduleInfo, enabled: true } saveSettings() // 2. 调用生命周期钩子 try { const config = moduleRegistry.getConfig(moduleId) await config?.lifecycle?.onEnable?.() } catch (e) { console.error(`[toggleModule] Module onEnable hook failed for "${moduleId}":`, e) logger.error(`模块 "${moduleId}" onEnable 钩子失败: ${e}`) } // 3. 恢复搜索项 try { const searchStore = useSearchStore() const config = moduleRegistry.getConfig(moduleId) if (config?.searchItems) { config.searchItems.forEach((item, index) => { searchStore.registerItem({ id: `${moduleId}-search-${index}`, moduleId, title: item.title, description: item.description, keywords: item.keywords }) }) } } catch (e) { console.error(`[toggleModule] Failed to register search items for "${moduleId}":`, e) logger.error(`启用模块 "${moduleId}" 时注册搜索项失败: ${e}`) } // 4. 如果配置了 autoStart,启动进程 if (moduleInfo.hasProcess) { const config = moduleRegistry.getConfig(moduleId) if (config?.process?.autoStart) { try { const processStore = useProcessStore() toast.loading(`正在启动 ${moduleInfo.name} 后台进程...`, { id: `start-${moduleId}` }) await processStore.startByModule(moduleId) toast.success(`${moduleInfo.name} 进程已启动`, { id: `start-${moduleId}` }) } catch (e) { console.error(`[toggleModule] Failed to start process for "${moduleId}":`, e) logger.error(`启动模块 "${moduleId}" 进程失败: ${e}`) toast.error(`启动 ${moduleInfo.name} 进程失败`, { id: `start-${moduleId}` }) } } } logger.info(`已启用模块: ${moduleInfo.name}`) toast.success(`已启用 ${moduleInfo.name}`) } return true } catch (e) { console.error(`[toggleModule] Failed to toggle module "${moduleId}":`, e) logger.error(`模块 "${moduleId}" 切换失败: ${(e as Error).message}`) toast.error(`操作失败: ${(e as Error).message}`) return false } finally { togglingModules.value.delete(moduleId) } } const setTheme = async (newTheme: Theme) => { theme.value = newTheme if (newTheme !== 'system' && effect.value === 'acrylic') { effect.value = 'mica' } await applyTheme() saveSettings() } const setEffect = async (newEffect: EffectType) => { effect.value = newEffect await applyEffect() saveSettings() } const toggleAutoStart = async (checked?: boolean) => { const targetState = checked !== undefined ? checked : !isAutoStart.value const previousState = isAutoStart.value try { isAutoStart.value = targetState if (targetState) { await enable() } else { await disable() } saveSettings() } catch (e) { isAutoStart.value = previousState console.error('Failed to toggle auto-start:', e) throw e } } const applyTheme = async () => { const root = document.documentElement root.classList.remove('dark') let isDark = false if (theme.value === 'dark') { isDark = true root.classList.add('dark') } else if (theme.value === 'system') { // 通过 Tauri 获取系统真实主题,避免 matchMedia 被 setTheme 污染 try { const tauriWindow = getCurrentWindow() const sysTheme = await tauriWindow.theme() isDark = sysTheme === 'dark' } catch { isDark = window.matchMedia('(prefers-color-scheme: dark)').matches } if (isDark) { root.classList.add('dark') } } try { const tauriWindow = getCurrentWindow() if (theme.value === 'system') { // 让窗口原生跟随系统主题,而不是固定成当前匹配值 // 固定主题会导致系统切换深浅色时窗口标题栏等原生 UI 不同步 await tauriWindow.setTheme(null) } else { await tauriWindow.setTheme(isDark ? 'dark' : 'light') } } catch (e) { // 非 Tauri 环境下忽略 } await applyEffect() } const applyEffect = async () => { const root = document.documentElement root.classList.remove('effect-mica', 'effect-acrylic', 'effect-normal') root.classList.add(`effect-${effect.value}`) try { const tauriWindow = getCurrentWindow() const isDark = root.classList.contains('dark') await tauriWindow.clearEffects() if (effect.value === 'normal') { await tauriWindow.setBackgroundColor(isDark ? '#0f172a' : '#ffffff') } else if (effect.value === 'mica') { const micaEffect = (isDark ? 'micaDark' : 'micaLight') as unknown as Effect await tauriWindow.setEffects({ effects: [micaEffect], state: EffectState.FollowsWindowActiveState, color: isDark ? [30, 41, 59, 0] : [248, 250, 252, 0] }) await tauriWindow.setBackgroundColor('#00000000') } else if (effect.value === 'acrylic') { await tauriWindow.setEffects({ effects: [Effect.Acrylic], state: EffectState.FollowsWindowActiveState, color: isDark ? [30, 41, 59, 0] : [248, 250, 252, 0] }) await tauriWindow.setBackgroundColor('#00000000') } } catch (e) { // 非 Tauri 环境下忽略 } // 切换效果后修复 snap-layout 子窗口背景(clearEffects 触发重绘会导致白色背景显现) try { const { invoke } = await import('@tauri-apps/api/core') invoke('fix_snap_background').catch(() => {}) } catch { /* 非 Tauri 环境忽略 */ } } // 系统主题变化时,若当前为"跟随系统"模式,同步更新 DOM 和窗口效果。 // 使用 Tauri 的 onThemeChanged 事件(而非 matchMedia),因为 setTheme('dark'/'light') // 会改变 WebView 的 prefers-color-scheme 媒体查询结果,导致 matchMedia 误触发。 // // 注意:onThemeChanged 也会被 setTheme('dark'/'light') 触发(非系统真实变化)。 // 因此 systemDark 只在 theme.value === 'system' 时更新: // - system 模式下 setTheme(null) 不固定主题,onThemeChanged 反映系统真实色 // - light/dark 模式下 setTheme 引起的 onThemeChanged 被忽略,systemDark 保持不变 // - 用户切回 system 模式时,setTheme(null) 会触发 onThemeChanged 反映系统真实色,自动修正 const handleSystemThemeChange = async (newTheme: 'light' | 'dark' | null) => { if (theme.value === 'system') { systemDark.value = newTheme === 'dark' const root = document.documentElement if (newTheme === 'dark') { root.classList.add('dark') } else { root.classList.remove('dark') } // 窗口已通过 setTheme(null) 原生跟随系统,无需再手动 setTheme await applyEffect() } } const init = async () => { try { await loadSettings() try { const saved = localStorage.getItem(STORAGE_KEY) const savedAutoStart = saved ? JSON.parse(saved).isAutoStart : false const systemAutoStart = await isEnabled() isAutoStart.value = systemAutoStart if (savedAutoStart !== systemAutoStart) { if (savedAutoStart) { await enable() isAutoStart.value = true } else { await disable() isAutoStart.value = false } saveSettings() } } catch (e) { console.error('Failed to sync auto-start during init:', e) } await applyTheme() // 使用 Tauri 的 onThemeChanged 监听系统主题变化,而非 matchMedia。 // 因为 setTheme('dark'/'light') 会改变 WebView 的 prefers-color-scheme 媒体查询, // 导致 matchMedia 在非 system 模式下也误触发(虽有 theme.value 守卫,但更可靠)。 try { const tauriWindow = getCurrentWindow() await tauriWindow.onThemeChanged(({ payload }) => { handleSystemThemeChange(payload) }) } catch { // 非 Tauri 环境回退到 matchMedia const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') mediaQuery.addEventListener('change', (e) => { handleSystemThemeChange(e.matches ? 'dark' : 'light') }) } isInitialized.value = true } finally { try { const tauriWindow = getCurrentWindow() await tauriWindow.show() } catch (e) { // 非 Tauri 环境下忽略 } } } return { theme, effect, systemDark, isAutoStart, isInitialized, modules, moduleOrder, enabledModules, togglingModules, getModule, toggleModule, reorderModules, setTheme, setEffect, toggleAutoStart, applyTheme, applyEffect, init, loadSettings } })