主界面修改及代理模块初始化
This commit is contained in:
+299
-24
@@ -1,48 +1,286 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
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 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 = 'thing_app_settings'
|
||||
|
||||
/** 从模块注册表初始化模块元信息 */
|
||||
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<Theme>('system')
|
||||
const effect = ref<EffectType>('mica')
|
||||
const isAutoStart = ref(false)
|
||||
const isInitialized = ref(false)
|
||||
const modules = ref<ModuleInfo[]>(initModulesFromRegistry())
|
||||
const moduleOrder = ref<string[]>(initModuleOrder())
|
||||
|
||||
const loadSettings = () => {
|
||||
/** 正在处理切换的模块 ID 集合(防止重复点击) */
|
||||
const togglingModules = ref<Set<string>>(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.isAutoStart !== undefined) isAutoStart.value = settings.isAutoStart
|
||||
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
|
||||
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'
|
||||
}
|
||||
@@ -56,9 +294,23 @@ export const useAppStore = defineStore('app', () => {
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
const toggleAutoStart = () => {
|
||||
isAutoStart.value = !isAutoStart.value
|
||||
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 () => {
|
||||
@@ -80,7 +332,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
const tauriWindow = getCurrentWindow()
|
||||
await tauriWindow.setTheme(isDark ? 'dark' : 'light')
|
||||
} catch (e) {
|
||||
console.error('Failed to set window theme:', e)
|
||||
// 非 Tauri 环境下忽略
|
||||
}
|
||||
|
||||
await applyEffect()
|
||||
@@ -95,25 +347,19 @@ export const useAppStore = defineStore('app', () => {
|
||||
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') {
|
||||
// 浅色用 micaLight,深色用 micaDark。
|
||||
// 注意:micaDark 仅在系统处于深色模式时才会渲染为深色(Windows 限制)。
|
||||
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') {
|
||||
// Acrylic:亚克力效果,color 使用半透明 RGBA
|
||||
await tauriWindow.setEffects({
|
||||
effects: [Effect.Acrylic],
|
||||
state: EffectState.FollowsWindowActiveState,
|
||||
@@ -122,7 +368,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
await tauriWindow.setBackgroundColor('#00000000')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to set window effects:', e)
|
||||
// 非 Tauri 环境下忽略
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,8 +384,8 @@ export const useAppStore = defineStore('app', () => {
|
||||
try {
|
||||
const tauriWindow = getCurrentWindow()
|
||||
await tauriWindow.setTheme(e.matches ? 'dark' : 'light')
|
||||
} catch (err) {
|
||||
console.error('Failed to update window theme on system change:', err)
|
||||
} catch (e) {
|
||||
// 非 Tauri 环境下忽略
|
||||
}
|
||||
|
||||
await applyEffect()
|
||||
@@ -148,8 +394,29 @@ export const useAppStore = defineStore('app', () => {
|
||||
|
||||
const init = async () => {
|
||||
try {
|
||||
loadSettings()
|
||||
// applyTheme 内部已调用 applyEffect,无需重复调用
|
||||
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()
|
||||
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
@@ -158,9 +425,10 @@ export const useAppStore = defineStore('app', () => {
|
||||
isInitialized.value = true
|
||||
} finally {
|
||||
try {
|
||||
await getCurrentWindow().show()
|
||||
const tauriWindow = getCurrentWindow()
|
||||
await tauriWindow.show()
|
||||
} catch (e) {
|
||||
console.error('Failed to show window:', e)
|
||||
// 非 Tauri 环境下忽略
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,6 +438,13 @@ export const useAppStore = defineStore('app', () => {
|
||||
effect,
|
||||
isAutoStart,
|
||||
isInitialized,
|
||||
modules,
|
||||
moduleOrder,
|
||||
enabledModules,
|
||||
togglingModules,
|
||||
getModule,
|
||||
toggleModule,
|
||||
reorderModules,
|
||||
setTheme,
|
||||
setEffect,
|
||||
toggleAutoStart,
|
||||
@@ -178,4 +453,4 @@ export const useAppStore = defineStore('app', () => {
|
||||
init,
|
||||
loadSettings
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user