import { useSyncExternalStore } from 'react'; import { CONFIG_KEYS, DEFAULT_LANGUAGE, DEFAULT_THEME_MODE, IPC_EVENTS } from '../lib/constants'; import { hasHostApiBridge, hostApiFetch, invokeIpc, onIpc } from '../lib/host-api'; import { applyThemeModeToDocument, detectSystemTheme, resolveAppliedTheme, watchSystemTheme } from '../lib/theme'; import { detectRuntimePlatform, resolveWindowIdentity } from '../lib/runtime'; import { i18n, setLocale as setI18nLocale } from '../i18n'; import { detectSystemLanguage, resolveSupportedLanguage } from '../i18n/resolver'; import type { ConfigValueMap, LanguageCode, ResolvedThemeMode, RuntimePlatform, ThemeMode, WindowIdentity, WindowName, } from '../types/runtime'; export interface SettingsState { initialized: boolean; platform: RuntimePlatform; windowId: string | number | null; windowName: WindowName; themeMode: ThemeMode; systemTheme: ResolvedThemeMode; appliedTheme: ResolvedThemeMode; language: LanguageCode; primaryColor: string; fontSize: number; minimizeToTray: boolean; providerId: string | null; defaultModel: string | null; } const STORAGE_PREFIX = 'zn-ai-react:'; const listeners = new Set<() => void>(); let initPromise: Promise | null = null; let unsubscribeThemeWatcher: (() => void) | null = null; let unsubscribeThemeEvent: (() => void) | null = null; function getStorageValue(key: string): T | undefined { if (typeof window === 'undefined') return undefined; const raw = window.localStorage.getItem(`${STORAGE_PREFIX}${key}`); if (!raw) return undefined; try { return JSON.parse(raw) as T; } catch { return raw as unknown as T; } } function setStorageValue(key: string, value: unknown): void { if (typeof window === 'undefined') return; window.localStorage.setItem(`${STORAGE_PREFIX}${key}`, JSON.stringify(value)); } function createInitialState(): SettingsState { const systemTheme = detectSystemTheme(); const systemLanguage = detectSystemLanguage(); return { initialized: false, platform: detectRuntimePlatform(), windowId: null, windowName: 'main', themeMode: DEFAULT_THEME_MODE, systemTheme, appliedTheme: resolveAppliedTheme(DEFAULT_THEME_MODE, systemTheme), language: systemLanguage ?? DEFAULT_LANGUAGE, primaryColor: '#1677ff', fontSize: 14, minimizeToTray: false, providerId: null, defaultModel: null, }; } let state: SettingsState = createInitialState(); function emit(): void { for (const listener of listeners) { listener(); } } function patchState(patch: Partial): SettingsState { state = { ...state, ...patch }; emit(); return state; } async function readConfigValue(key: keyof ConfigValueMap, fallback: T): Promise { try { if (hasHostApiBridge()) { const value = await invokeIpc(IPC_EVENTS.GET_CONFIG, key); return (typeof value === 'undefined' || value === null ? fallback : value) as T; } } catch { // fall back to local storage below } const stored = getStorageValue(String(key)); return typeof stored === 'undefined' ? fallback : stored; } async function writeConfigValue(key: keyof ConfigValueMap, value: T): Promise { try { if (hasHostApiBridge()) { await invokeIpc(IPC_EVENTS.SET_CONFIG, key, value); return; } } catch { // fall back to local storage below } setStorageValue(String(key), value); } async function readThemeMode(): Promise { try { if (hasHostApiBridge()) { const value = await invokeIpc(IPC_EVENTS.GET_THEME_MODE); if (value === true) return 'dark'; if (value === false) return 'light'; if (value === 'light' || value === 'dark' || value === 'system') return value; } } catch { // fallback below } return (getStorageValue('themeMode') ?? DEFAULT_THEME_MODE) as ThemeMode; } async function writeThemeMode(themeMode: ThemeMode): Promise { try { if (hasHostApiBridge()) { await invokeIpc(IPC_EVENTS.SET_THEME_MODE, themeMode); return; } } catch { // fallback below } setStorageValue('themeMode', themeMode); } function applyLocale(locale: LanguageCode): void { if (typeof document !== 'undefined') { document.documentElement.lang = locale; } setI18nLocale(locale); } function applyTheme(themeMode: ThemeMode, systemTheme: ResolvedThemeMode): ResolvedThemeMode { const appliedTheme = applyThemeModeToDocument(themeMode, systemTheme); if (typeof document !== 'undefined') { document.documentElement.dataset.themeMode = themeMode; } return appliedTheme; } function syncSystemTheme(): void { if (unsubscribeThemeWatcher) return; unsubscribeThemeWatcher = watchSystemTheme((systemTheme) => { const appliedTheme = state.themeMode === 'system' ? applyTheme(state.themeMode, systemTheme) : state.appliedTheme; patchState({ systemTheme, appliedTheme, }); }); } async function syncWindowIdentity(): Promise { const identity = await resolveWindowIdentity(); patchState({ platform: identity.platform, windowId: identity.windowId, windowName: identity.windowName, }); return identity; } async function syncThemeEvent(): Promise { if (unsubscribeThemeEvent || typeof window === 'undefined' || !window.api?.on) return; unsubscribeThemeEvent = onIpc(IPC_EVENTS.THEME_MODE_UPDATED, (payload: boolean | ThemeMode) => { const themeMode: ThemeMode = typeof payload === 'boolean' ? (payload ? 'dark' : 'light') : payload; const appliedTheme = applyTheme(themeMode, state.systemTheme); patchState({ themeMode, appliedTheme, }); }); } async function hydrate(): Promise { if (initPromise) return initPromise; initPromise = (async () => { const identity = await syncWindowIdentity(); const systemTheme = detectSystemTheme(); const systemLanguage = detectSystemLanguage(); const [themeMode, language, fontSize, minimizeToTray, primaryColor, providerId, defaultModel] = await Promise.all([ readThemeMode(), readConfigValue(CONFIG_KEYS.LANGUAGE, systemLanguage), readConfigValue(CONFIG_KEYS.FONT_SIZE, 14), readConfigValue(CONFIG_KEYS.MINIMIZE_TO_TRAY, false), readConfigValue(CONFIG_KEYS.PRIMARY_COLOR, '#1677ff'), readConfigValue(CONFIG_KEYS.PROVIDER, null), readConfigValue(CONFIG_KEYS.DEFAULT_MODEL, null), ]); const resolvedLanguage = resolveSupportedLanguage(language ?? systemLanguage); const appliedTheme = applyTheme(themeMode, systemTheme); patchState({ initialized: true, platform: identity.platform, windowId: identity.windowId, windowName: identity.windowName, themeMode, systemTheme, appliedTheme, language: resolvedLanguage, primaryColor: primaryColor ?? '#1677ff', fontSize: fontSize ?? 14, minimizeToTray: Boolean(minimizeToTray), providerId: providerId ?? null, defaultModel: defaultModel ?? null, }); applyLocale(resolvedLanguage); syncSystemTheme(); await syncThemeEvent(); return state; })(); return initPromise; } async function setThemeMode(themeMode: ThemeMode): Promise { const nextThemeMode = themeMode === 'light' || themeMode === 'dark' || themeMode === 'system' ? themeMode : DEFAULT_THEME_MODE; if (state.themeMode === nextThemeMode && state.initialized) return state; const appliedTheme = applyTheme(nextThemeMode, state.systemTheme); patchState({ themeMode: nextThemeMode, appliedTheme, }); await writeThemeMode(nextThemeMode); return state; } async function setLanguage(language: string | null | undefined, persist = true): Promise { const resolved = resolveSupportedLanguage(language, state.language); if (state.language === resolved && state.initialized) return state; applyLocale(resolved); patchState({ language: resolved, }); if (persist) { await writeConfigValue(CONFIG_KEYS.LANGUAGE, resolved); } return state; } async function setFontSize(fontSize: number, persist = true): Promise { const next = Number.isFinite(fontSize) ? fontSize : state.fontSize; if (state.fontSize === next && state.initialized) return state; patchState({ fontSize: next, }); if (persist) { await writeConfigValue(CONFIG_KEYS.FONT_SIZE, next); } return state; } async function setMinimizeToTray(minimizeToTray: boolean, persist = true): Promise { const next = Boolean(minimizeToTray); if (state.minimizeToTray === next && state.initialized) return state; patchState({ minimizeToTray: next, }); if (persist) { await writeConfigValue(CONFIG_KEYS.MINIMIZE_TO_TRAY, next); } return state; } async function setPrimaryColor(primaryColor: string, persist = true): Promise { const next = primaryColor || '#1677ff'; if (state.primaryColor === next && state.initialized) return state; patchState({ primaryColor: next, }); if (persist) { await writeConfigValue(CONFIG_KEYS.PRIMARY_COLOR, next); } return state; } function getSnapshot(): SettingsState { return state; } function subscribe(listener: () => void): () => void { listeners.add(listener); return () => listeners.delete(listener); } export const settingsStore = { init: hydrate, getState: getSnapshot, subscribe, setThemeMode, setLanguage, setFontSize, setMinimizeToTray, setPrimaryColor, hostApiFetch, }; export function useSettingsStore(selector?: (state: SettingsState) => T): T { const select = selector ?? ((current: SettingsState) => current as unknown as T); return useSyncExternalStore(subscribe, () => select(getSnapshot()), () => select(getSnapshot())); } export function getSettingsState(): SettingsState { return getSnapshot(); } export async function initSettingsStore(): Promise { return hydrate(); } export async function updateThemeMode(themeMode: ThemeMode): Promise { return setThemeMode(themeMode); } export async function updateLanguage(language: string | null | undefined, persist = true): Promise { return setLanguage(language, persist); } export async function updateFontSize(fontSize: number, persist = true): Promise { return setFontSize(fontSize, persist); } export async function updatePrimaryColor(primaryColor: string, persist = true): Promise { return setPrimaryColor(primaryColor, persist); } export async function updateMinimizeToTray(minimizeToTray: boolean, persist = true): Promise { return setMinimizeToTray(minimizeToTray, persist); } export { i18n };