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.
This commit is contained in:
374
src-react/stores/settings.ts
Normal file
374
src-react/stores/settings.ts
Normal file
@@ -0,0 +1,374 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user