feat: enhance theme management and image caching functionality
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
require("electron");
|
||||
require("./main-3jJaZPgE.js");
|
||||
require("./main-BekteP6H.js");
|
||||
require("electron-squirrel-startup");
|
||||
require("electron-log");
|
||||
require("bytenode");
|
||||
|
||||
@@ -3,6 +3,7 @@ import { CONFIG_KEYS } from '@lib/constants'
|
||||
import { setupMainWindow } from './wins';
|
||||
import started from 'electron-squirrel-startup'
|
||||
import configManager from '@electron/service/config-service'
|
||||
import themeManager from '@electron/service/theme-service'
|
||||
import { runTaskOperationService } from '@electron/process/runTaskOperationService'
|
||||
import { initScriptStoreService } from '@electron/service/script-store-service'
|
||||
import log from 'electron-log';
|
||||
@@ -154,6 +155,7 @@ if (started) {
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
await configManager.init();
|
||||
await themeManager.init();
|
||||
|
||||
gatewayManager.init();
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ const DEFAULT_CONFIG: IConfig = {
|
||||
[CONFIG_KEYS.PROVIDER]: '',
|
||||
[CONFIG_KEYS.DEFAULT_MODEL]: null,
|
||||
[CONFIG_KEYS.SELECTED_CHANNELS]: [],
|
||||
[CONFIG_KEYS.IMAGE_CACHE]: [],
|
||||
}
|
||||
|
||||
export class ConfigService {
|
||||
|
||||
@@ -8,15 +8,17 @@ class ThemeService {
|
||||
private _isDark: boolean = nativeTheme.shouldUseDarkColors;
|
||||
|
||||
constructor() {
|
||||
const themeMode = configManager.get(CONFIG_KEYS.THEME_MODE);
|
||||
if (themeMode) {
|
||||
nativeTheme.themeSource = themeMode;
|
||||
this._isDark = nativeTheme.shouldUseDarkColors;
|
||||
}
|
||||
this._setupIpcEvent();
|
||||
logManager.info('ThemeService initialized successfully.');
|
||||
}
|
||||
|
||||
async init() {
|
||||
const themeMode = configManager.get(CONFIG_KEYS.THEME_MODE);
|
||||
nativeTheme.themeSource = themeMode;
|
||||
this._isDark = nativeTheme.shouldUseDarkColors;
|
||||
logManager.info('ThemeService async init completed.');
|
||||
}
|
||||
|
||||
private _setupIpcEvent() {
|
||||
ipcMain.handle(IPC_EVENTS.SET_THEME_MODE, (_e, mode: ThemeMode) => {
|
||||
nativeTheme.themeSource = mode;
|
||||
@@ -25,7 +27,7 @@ class ThemeService {
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_EVENTS.GET_THEME_MODE, () => {
|
||||
return nativeTheme.themeSource;
|
||||
return configManager.get(CONFIG_KEYS.THEME_MODE);
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_EVENTS.IS_DARK_THEME, () => {
|
||||
|
||||
@@ -29,24 +29,26 @@ const isMac = process.platform === 'darwin';
|
||||
const isWindows = process.platform === 'win32';
|
||||
const useCustomTitleBar = isWindows;
|
||||
|
||||
const SHARED_WINDOW_OPTIONS = {
|
||||
frame: isMac || !useCustomTitleBar,
|
||||
titleBarStyle: isMac ? 'hiddenInset' : useCustomTitleBar ? 'hidden' : 'default',
|
||||
trafficLightPosition: isMac ? { x: 16, y: 16 } : undefined,
|
||||
show: false,
|
||||
title: 'NIANXX',
|
||||
darkTheme: themeManager.isDark,
|
||||
backgroundColor: themeManager.isDark ? '#2C2C2C' : '#FFFFFF',
|
||||
webPreferences: {
|
||||
nodeIntegration: false, // 禁用 Node.js 集成,提高安全性
|
||||
contextIsolation: true, // 启用上下文隔离,防止渲染进程访问主进程 API
|
||||
sandbox: true, // 启用沙箱模式,进一步增强安全性
|
||||
backgroundThrottling: false,
|
||||
preload: MAIN_WINDOW_VITE_DEV_SERVER_URL
|
||||
? path.join(process.cwd(), 'dist-electron/preload/preload.js')
|
||||
: path.join(__dirname, 'preload.js'),
|
||||
},
|
||||
} as BrowserWindowConstructorOptions;
|
||||
function getSharedWindowOptions(): BrowserWindowConstructorOptions {
|
||||
return {
|
||||
frame: isMac || !useCustomTitleBar,
|
||||
titleBarStyle: isMac ? 'hiddenInset' : useCustomTitleBar ? 'hidden' : 'default',
|
||||
trafficLightPosition: isMac ? { x: 16, y: 16 } : undefined,
|
||||
show: false,
|
||||
title: 'NIANXX',
|
||||
darkTheme: themeManager.isDark,
|
||||
backgroundColor: themeManager.isDark ? '#2C2C2C' : '#FFFFFF',
|
||||
webPreferences: {
|
||||
nodeIntegration: false, // 禁用 Node.js 集成,提高安全性
|
||||
contextIsolation: true, // 启用上下文隔离,防止渲染进程访问主进程 API
|
||||
sandbox: true, // 启用沙箱模式,进一步增强安全性
|
||||
backgroundThrottling: false,
|
||||
preload: MAIN_WINDOW_VITE_DEV_SERVER_URL
|
||||
? path.join(process.cwd(), 'dist-electron/preload/preload.js')
|
||||
: path.join(__dirname, 'preload.js'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
class WindowService {
|
||||
private static _instance: WindowService;
|
||||
@@ -218,7 +220,7 @@ class WindowService {
|
||||
return this._isHiddenWin(name)
|
||||
? this._winStates[name].instance as BrowserWindow
|
||||
: new BrowserWindow({
|
||||
...SHARED_WINDOW_OPTIONS,
|
||||
...getSharedWindowOptions(),
|
||||
icon: this._logo,
|
||||
...opts,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createI18n, type I18n, type I18nOptions } from 'vue-i18n';
|
||||
import { SUPPORTED_LANGUAGE_CODES, SUPPORTED_LANGUAGES, NAMESPACES, type LanguageCode } from './constants';
|
||||
import { resolveSupportedLanguage, detectSystemLanguage } from './resolver';
|
||||
import { resolveSupportedLanguage } from './resolver';
|
||||
|
||||
// 使用 import.meta.glob 动态加载所有语言文件
|
||||
// 文件路径模式:./locales/{语言}/{命名空间}.json
|
||||
@@ -34,32 +34,12 @@ function buildResources() {
|
||||
return resources;
|
||||
}
|
||||
|
||||
// 获取持久化的语言设置(稍后由 Pinia store 提供)
|
||||
function getPersistedLanguage(): LanguageCode | null {
|
||||
try {
|
||||
const saved = localStorage.getItem('zn-language');
|
||||
return saved && SUPPORTED_LANGUAGE_CODES.includes(saved as LanguageCode) ? saved as LanguageCode : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 确定初始语言:持久化设置 > 系统语言 > 默认中文
|
||||
function determineInitialLocale(): LanguageCode {
|
||||
const persisted = getPersistedLanguage();
|
||||
if (persisted) return persisted;
|
||||
|
||||
const systemLang = detectSystemLanguage();
|
||||
return systemLang;
|
||||
}
|
||||
|
||||
async function createI18nInstance() {
|
||||
const resources = buildResources();
|
||||
const initialLocale = determineInitialLocale();
|
||||
|
||||
const options: I18nOptions = {
|
||||
legacy: false,
|
||||
locale: initialLocale,
|
||||
locale: 'zh',
|
||||
fallbackLocale: 'zh',
|
||||
messages: resources, // 使用构建的资源对象
|
||||
availableLocales: SUPPORTED_LANGUAGE_CODES,
|
||||
@@ -85,11 +65,6 @@ export async function setLanguage(lang: LanguageCode, _i18n?: I18n) {
|
||||
}
|
||||
|
||||
(__i18n.global.locale as unknown as { value: LanguageCode }).value = lang;
|
||||
|
||||
// 持久化到 localStorage(稍后由 Pinia store 处理)
|
||||
try {
|
||||
localStorage.setItem('zn-language', lang);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export function getLanguage() {
|
||||
|
||||
@@ -103,6 +103,7 @@ export enum CONFIG_KEYS {
|
||||
AUTO_CHECK_UPDATE = 'autoCheckUpdate',
|
||||
AUTO_DOWNLOAD_UPDATE = 'autoDownloadUpdate',
|
||||
SELECTED_CHANNELS = 'selectedChannels',
|
||||
IMAGE_CACHE = 'imageCache',
|
||||
}
|
||||
|
||||
export enum MENU_IDS {
|
||||
|
||||
@@ -20,6 +20,8 @@ export interface IConfig {
|
||||
[CONFIG_KEYS.DEFAULT_MODEL]?: string | null;
|
||||
// 选中的渠道
|
||||
[CONFIG_KEYS.SELECTED_CHANNELS]: Array<{ id: string; channelName: string; channelUrl: string }>;
|
||||
// 图片缓存
|
||||
[CONFIG_KEYS.IMAGE_CACHE]: Array<[string, any]>;
|
||||
}
|
||||
|
||||
export interface Provider {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { extractText, isToolOnlyMessage, isToolResultRole, isInternalMessage } f
|
||||
import { hostApiFetch } from '@lib/host-api'
|
||||
import { gatewayRpc, onGatewayEvent } from '@lib/gateway-client'
|
||||
import { useProviderStore } from '@stores/providers'
|
||||
import { IPC_EVENTS, CONFIG_KEYS } from '@lib/constants'
|
||||
|
||||
// ── Constants ───────────────────────────────────────────────────
|
||||
const DEFAULT_SESSION_KEY = 'agent:main:main'
|
||||
@@ -32,7 +33,6 @@ let _lastLoadSessionsAt = 0
|
||||
const _historyLoadInFlight = new Map<string, Promise<void>>()
|
||||
const _lastHistoryLoadAtBySession = new Map<string, number>()
|
||||
const _chatEventDedupe = new Map<string, number>()
|
||||
const IMAGE_CACHE_KEY = 'zn-ai:image-cache'
|
||||
const IMAGE_CACHE_MAX = 100
|
||||
|
||||
// ── Helpers: Timers ─────────────────────────────────────────────
|
||||
@@ -90,29 +90,35 @@ function isDuplicateChatEvent(eventState: string, event: Record<string, unknown>
|
||||
}
|
||||
|
||||
// ── Helpers: Image Cache ────────────────────────────────────────
|
||||
function loadImageCache(): Map<string, AttachedFileMeta> {
|
||||
let _imageCache: Map<string, AttachedFileMeta> = new Map()
|
||||
let _imageCacheInitialized = false
|
||||
|
||||
async function initImageCache(): Promise<void> {
|
||||
if (_imageCacheInitialized) return
|
||||
_imageCache = await loadImageCache()
|
||||
_imageCacheInitialized = true
|
||||
}
|
||||
|
||||
async function loadImageCache(): Promise<Map<string, AttachedFileMeta>> {
|
||||
try {
|
||||
const raw = localStorage.getItem(IMAGE_CACHE_KEY)
|
||||
if (raw) {
|
||||
const entries = JSON.parse(raw) as Array<[string, AttachedFileMeta]>
|
||||
return new Map(entries)
|
||||
const raw = await window.api.invoke(IPC_EVENTS.GET_CONFIG, CONFIG_KEYS.IMAGE_CACHE)
|
||||
if (Array.isArray(raw)) {
|
||||
return new Map(raw as Array<[string, AttachedFileMeta]>)
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
return new Map()
|
||||
}
|
||||
|
||||
function saveImageCache(cache: Map<string, AttachedFileMeta>): void {
|
||||
async function saveImageCache(cache: Map<string, AttachedFileMeta>): Promise<void> {
|
||||
try {
|
||||
const entries = Array.from(cache.entries())
|
||||
const trimmed = entries.length > IMAGE_CACHE_MAX
|
||||
? entries.slice(entries.length - IMAGE_CACHE_MAX)
|
||||
: entries
|
||||
localStorage.setItem(IMAGE_CACHE_KEY, JSON.stringify(trimmed))
|
||||
await window.api.invoke(IPC_EVENTS.SET_CONFIG, CONFIG_KEYS.IMAGE_CACHE, trimmed)
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const _imageCache = loadImageCache()
|
||||
|
||||
// ── Helpers: Timestamp ──────────────────────────────────────────
|
||||
function toMs(ts: number): number {
|
||||
return ts < 1e12 ? ts * 1000 : ts
|
||||
@@ -918,6 +924,7 @@ export const useChatStore = defineStore('chat', {
|
||||
try {
|
||||
// Cache image attachments
|
||||
if (attachments && attachments.length > 0) {
|
||||
await initImageCache()
|
||||
for (const a of attachments) {
|
||||
_imageCache.set(a.stagedPath, {
|
||||
fileName: a.fileName,
|
||||
@@ -926,7 +933,7 @@ export const useChatStore = defineStore('chat', {
|
||||
preview: a.preview,
|
||||
})
|
||||
}
|
||||
saveImageCache(_imageCache)
|
||||
await saveImageCache(_imageCache)
|
||||
}
|
||||
|
||||
let messageContent = trimmed
|
||||
|
||||
@@ -2,9 +2,7 @@ import { defineStore } from 'pinia';
|
||||
import { i18n, setLanguage, getLanguage, type LanguageCode } from '@src/i18n';
|
||||
import { SUPPORTED_LANGUAGES, SUPPORTED_LANGUAGE_CODES } from '@src/i18n/constants';
|
||||
import { resolveSupportedLanguage, detectSystemLanguage } from '@src/i18n/resolver';
|
||||
|
||||
// 持久化键
|
||||
const STORAGE_KEY = 'zn-language';
|
||||
import { IPC_EVENTS, CONFIG_KEYS } from '@lib/constants';
|
||||
|
||||
interface LocaleState {
|
||||
language: LanguageCode;
|
||||
@@ -37,8 +35,8 @@ export const useLocaleStore = defineStore('locale', {
|
||||
if (this.initialized) return;
|
||||
|
||||
try {
|
||||
// 1. 尝试从 localStorage 读取持久化设置
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
// 1. 尝试从 electron-store 读取持久化设置
|
||||
const saved = await window.api.invoke(IPC_EVENTS.GET_CONFIG, CONFIG_KEYS.LANGUAGE);
|
||||
let lang: LanguageCode = 'zh';
|
||||
|
||||
if (saved && SUPPORTED_LANGUAGE_CODES.includes(saved as LanguageCode)) {
|
||||
@@ -63,12 +61,12 @@ export const useLocaleStore = defineStore('locale', {
|
||||
/**
|
||||
* 设置语言
|
||||
* @param language 目标语言代码
|
||||
* @param persist 是否持久化到 localStorage(默认为 true)
|
||||
* @param persist 是否持久化到 electron-store(默认为 true)
|
||||
*/
|
||||
async setLanguage(language: LanguageCode, persist: boolean = true) {
|
||||
// 验证语言代码有效性
|
||||
const resolvedLang = resolveSupportedLanguage(language);
|
||||
|
||||
|
||||
if (resolvedLang === this.language) {
|
||||
return; // 语言未变化
|
||||
}
|
||||
@@ -80,14 +78,14 @@ export const useLocaleStore = defineStore('locale', {
|
||||
// 2. 更新 store 状态
|
||||
this.language = resolvedLang;
|
||||
|
||||
// 3. 持久化到 localStorage
|
||||
// 3. 持久化到 electron-store
|
||||
if (persist) {
|
||||
localStorage.setItem(STORAGE_KEY, resolvedLang);
|
||||
await window.api.invoke(IPC_EVENTS.SET_CONFIG, CONFIG_KEYS.LANGUAGE, resolvedLang);
|
||||
}
|
||||
|
||||
// 4. 触发语言变化事件(供其他组件监听)
|
||||
window.dispatchEvent(new CustomEvent('language-changed', {
|
||||
detail: { language: resolvedLang }
|
||||
window.dispatchEvent(new CustomEvent('language-changed', {
|
||||
detail: { language: resolvedLang }
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to set language:', error);
|
||||
|
||||
@@ -42,18 +42,12 @@ const applyThemeToDom = (theme: 'light' | 'dark') => {
|
||||
};
|
||||
|
||||
export const useThemeStore = defineStore('zn-ai-theme', {
|
||||
state: (): ThemeState => {
|
||||
// 从 localStorage 恢复缓存的主题,确保在初始化前 UI 不会闪烁
|
||||
const cachedTheme = typeof localStorage !== 'undefined' ? localStorage.getItem('zn-ai-theme-cache') as Theme : null;
|
||||
const initialTheme = (cachedTheme === 'light' || cachedTheme === 'dark' || cachedTheme === 'system') ? cachedTheme : 'system';
|
||||
|
||||
return {
|
||||
theme: initialTheme,
|
||||
isDark: false,
|
||||
systemTheme: 'light',
|
||||
initialized: false,
|
||||
};
|
||||
},
|
||||
state: (): ThemeState => ({
|
||||
theme: 'system',
|
||||
isDark: false,
|
||||
systemTheme: 'light',
|
||||
initialized: false,
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async init() {
|
||||
@@ -83,12 +77,7 @@ export const useThemeStore = defineStore('zn-ai-theme', {
|
||||
this.isDark = isDark;
|
||||
this.systemTheme = systemTheme;
|
||||
this.initialized = true;
|
||||
|
||||
// 缓存到 localStorage
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('zn-ai-theme-cache', savedTheme);
|
||||
}
|
||||
|
||||
|
||||
// 6. 监听系统主题变化
|
||||
if (typeof window !== 'undefined') {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
@@ -159,12 +148,7 @@ export const useThemeStore = defineStore('zn-ai-theme', {
|
||||
// 4. 更新 store 状态
|
||||
this.theme = theme;
|
||||
this.isDark = isDark;
|
||||
|
||||
// 5. 缓存到 localStorage
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('zn-ai-theme-cache', theme);
|
||||
}
|
||||
|
||||
|
||||
console.log('Theme changed:', { theme, appliedTheme, isDark });
|
||||
} catch (error) {
|
||||
console.error('Failed to set theme:', error);
|
||||
|
||||
@@ -37,52 +37,9 @@ const sessionCache = {
|
||||
}
|
||||
}
|
||||
|
||||
const localCache = {
|
||||
set (key: string, value: string) {
|
||||
if (!localStorage) {
|
||||
return
|
||||
}
|
||||
if (key != null && value != null) {
|
||||
localStorage.setItem(key, value)
|
||||
}
|
||||
},
|
||||
|
||||
get (key: string) {
|
||||
if (!localStorage) {
|
||||
return null
|
||||
}
|
||||
if (key == null) {
|
||||
return null
|
||||
}
|
||||
return localStorage.getItem(key)
|
||||
},
|
||||
|
||||
setJSON (key: string, jsonValue: any) {
|
||||
if (jsonValue != null) {
|
||||
this.set(key, JSON.stringify(jsonValue))
|
||||
}
|
||||
},
|
||||
|
||||
getJSON (key: string) {
|
||||
const value = this.get(key)
|
||||
if (value != null) {
|
||||
return JSON.parse(value)
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
remove (key: string) {
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 会话级缓存
|
||||
*/
|
||||
session: sessionCache,
|
||||
/**
|
||||
* 本地缓存
|
||||
*/
|
||||
local: localCache
|
||||
session: sessionCache
|
||||
}
|
||||
|
||||
@@ -1,41 +1,5 @@
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
/**
|
||||
* window.localStorage 浏览器永久缓存
|
||||
* @method set 设置永久缓存
|
||||
* @method get 获取永久缓存
|
||||
* @method remove 移除永久缓存
|
||||
* @method clear 移除全部永久缓存
|
||||
*/
|
||||
export const Local = {
|
||||
// 查看 v2.4.3版本更新日志
|
||||
setKey(key: string) {
|
||||
// @ts-ignore
|
||||
return `${__NEXT_NAME__}:${key}`;
|
||||
},
|
||||
|
||||
// 设置永久缓存
|
||||
set<T>(key: string, val: T) {
|
||||
window.localStorage.setItem(Local.setKey(key), JSON.stringify(val));
|
||||
},
|
||||
|
||||
// 获取永久缓存
|
||||
get(key: string) {
|
||||
let json = <string>window.localStorage.getItem(Local.setKey(key));
|
||||
return JSON.parse(json);
|
||||
},
|
||||
|
||||
// 移除永久缓存
|
||||
remove(key: string) {
|
||||
window.localStorage.removeItem(Local.setKey(key));
|
||||
},
|
||||
|
||||
// 移除全部永久缓存
|
||||
clear() {
|
||||
window.localStorage.clear();
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* window.sessionStorage 浏览器临时缓存
|
||||
* @method set 设置临时缓存
|
||||
@@ -81,8 +45,4 @@ export const Session = {
|
||||
return this.get('token');
|
||||
},
|
||||
|
||||
// 获取当前的租户
|
||||
getTenant() {
|
||||
return Local.get('tenantId') ? Local.get('tenantId') : 1;
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user