Files
zn-ai/src-react/stores/settings.ts
duanshuwen b1dea9a5c2 feat: implement task management store with IPC integration
- Added a new task store in `src-react/stores/task.ts` to manage tasks and their statuses.
- Implemented functions for creating, executing, and retrying tasks, along with handling task progress and completion.
- Introduced persistence for tasks using IPC.
- Created utility functions for normalizing room types and building subtasks.
- Added a new CSS file for global styles in `src-react/styles.css`.
- Created runtime types in `src-react/types/runtime.ts` and exported them.
- Updated the main entry points for Vue and React applications to support dynamic framework loading.
- Refactored chat model interfaces and utility functions into `src/shared/chat-model.ts`.
- Updated TypeScript configuration to include paths for React components and types.
- Enhanced Vite configuration to support both Vue and React frameworks.
2026-04-17 07:09:56 +08:00

375 lines
11 KiB
TypeScript

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<SettingsState> | null = null;
let unsubscribeThemeWatcher: (() => void) | null = null;
let unsubscribeThemeEvent: (() => void) | null = null;
function getStorageValue<T>(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>): SettingsState {
state = { ...state, ...patch };
emit();
return state;
}
async function readConfigValue<T>(key: keyof ConfigValueMap, fallback: T): Promise<T> {
try {
if (hasHostApiBridge()) {
const value = await invokeIpc<T>(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<T>(String(key));
return typeof stored === 'undefined' ? fallback : stored;
}
async function writeConfigValue<T>(key: keyof ConfigValueMap, value: T): Promise<void> {
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<ThemeMode> {
try {
if (hasHostApiBridge()) {
const value = await invokeIpc<ThemeMode | boolean>(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>('themeMode') ?? DEFAULT_THEME_MODE) as ThemeMode;
}
async function writeThemeMode(themeMode: ThemeMode): Promise<void> {
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<WindowIdentity> {
const identity = await resolveWindowIdentity();
patchState({
platform: identity.platform,
windowId: identity.windowId,
windowName: identity.windowName,
});
return identity;
}
async function syncThemeEvent(): Promise<void> {
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<SettingsState> {
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<LanguageCode>(CONFIG_KEYS.LANGUAGE, systemLanguage),
readConfigValue<number>(CONFIG_KEYS.FONT_SIZE, 14),
readConfigValue<boolean>(CONFIG_KEYS.MINIMIZE_TO_TRAY, false),
readConfigValue<string>(CONFIG_KEYS.PRIMARY_COLOR, '#1677ff'),
readConfigValue<string | null>(CONFIG_KEYS.PROVIDER, null),
readConfigValue<string | null>(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<SettingsState> {
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<SettingsState> {
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<SettingsState> {
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<SettingsState> {
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<SettingsState> {
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<T = SettingsState>(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<SettingsState> {
return hydrate();
}
export async function updateThemeMode(themeMode: ThemeMode): Promise<SettingsState> {
return setThemeMode(themeMode);
}
export async function updateLanguage(language: string | null | undefined, persist = true): Promise<SettingsState> {
return setLanguage(language, persist);
}
export async function updateFontSize(fontSize: number, persist = true): Promise<SettingsState> {
return setFontSize(fontSize, persist);
}
export async function updatePrimaryColor(primaryColor: string, persist = true): Promise<SettingsState> {
return setPrimaryColor(primaryColor, persist);
}
export async function updateMinimizeToTray(minimizeToTray: boolean, persist = true): Promise<SettingsState> {
return setMinimizeToTray(minimizeToTray, persist);
}
export { i18n };