diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 29433a2d..4e55e0b6 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -12,6 +12,7 @@ export enum IpcChannel { App_SetTrayOnClose = 'app:set-tray-on-close', App_RestartTray = 'app:restart-tray', App_SetTheme = 'app:set-theme', + App_SetCustomCss = 'app:set-custom-css', App_IsBinaryExist = 'app:is-binary-exist', App_GetBinaryPath = 'app:get-binary-path', diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 1a328114..816ad7d5 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -130,6 +130,22 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight) }) + // custom css + ipcMain.handle(IpcChannel.App_SetCustomCss, (event, css: string) => { + if (css === configManager.getCustomCss()) return + configManager.setCustomCss(css) + + // Broadcast to all windows including the mini window + const senderWindowId = event.sender.id + const windows = BrowserWindow.getAllWindows() + // 向其他窗口广播主题变化 + windows.forEach((win) => { + if (win.webContents.id !== senderWindowId) { + win.webContents.send('custom-css:update', css) + } + }) + }) + // clear cache ipcMain.handle(IpcChannel.App_ClearCache, async () => { const sessions = [session.defaultSession, session.fromPartition('persist:webview')] diff --git a/src/main/services/ConfigManager.ts b/src/main/services/ConfigManager.ts index 2dd53a02..053a0326 100644 --- a/src/main/services/ConfigManager.ts +++ b/src/main/services/ConfigManager.ts @@ -42,6 +42,14 @@ export class ConfigManager { this.set(ConfigKeys.Theme, theme) } + getCustomCss(): string { + return this.store.get('customCss', '') as string + } + + setCustomCss(css: string) { + this.store.set('customCss', css) + } + getLaunchToTray(): boolean { return !!this.get(ConfigKeys.LaunchToTray, false) } diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 536850f8..d7fcd590 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -29,6 +29,7 @@ declare global { setTrayOnClose: (isActive: boolean) => void restartTray: () => void setTheme: (theme: 'light' | 'dark') => void + setCustomCss: (css: string) => void reload: () => void clearCache: () => Promise<{ success: boolean; error?: string }> system: { diff --git a/src/preload/index.ts b/src/preload/index.ts index 882a15b7..d041d157 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -19,6 +19,7 @@ const api = { setTrayOnClose: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTrayOnClose, isActive), restartTray: () => ipcRenderer.invoke(IpcChannel.App_RestartTray), setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke(IpcChannel.App_SetTheme, theme), + setCustomCss: (css: string) => ipcRenderer.invoke(IpcChannel.App_SetCustomCss, css), openWebsite: (url: string) => ipcRenderer.invoke(IpcChannel.Open_Website, url), clearCache: () => ipcRenderer.invoke(IpcChannel.App_ClearCache), system: { diff --git a/src/renderer/src/components/MinApp/MinappPopupContainer.tsx b/src/renderer/src/components/MinApp/MinappPopupContainer.tsx index 3e68943d..c6bc2ae3 100644 --- a/src/renderer/src/components/MinApp/MinappPopupContainer.tsx +++ b/src/renderer/src/components/MinApp/MinappPopupContainer.tsx @@ -331,7 +331,7 @@ const MinappPopupContainer: React.FC = () => { height={'100%'} maskClosable={false} closeIcon={null} - style={{ marginLeft: 'var(--sidebar-width)' }}> + style={{ marginLeft: 'var(--sidebar-width)', backgroundColor: 'var(--color-background)' }}> {!isReady && ( { dispatch(setCustomCss(e.target.value))} + onChange={(e) => { + dispatch(setCustomCss(e.target.value)) + window.api.setCustomCss(e.target.value) + }} placeholder={t('settings.display.custom.css.placeholder')} style={{ minHeight: 200, diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index ece6d8cd..6ad8cfbc 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -286,6 +286,9 @@ const settingsSlice = createSlice({ setTheme: (state, action: PayloadAction) => { state.theme = action.payload }, + setCustomCss: (state, action: PayloadAction) => { + state.customCss = action.payload + }, setFontSize: (state, action: PayloadAction) => { state.fontSize = action.payload }, @@ -382,9 +385,6 @@ const settingsSlice = createSlice({ setPasteLongTextThreshold: (state, action: PayloadAction) => { state.pasteLongTextThreshold = action.payload }, - setCustomCss: (state, action: PayloadAction) => { - state.customCss = action.payload - }, setTopicNamingPrompt: (state, action: PayloadAction) => { state.topicNamingPrompt = action.payload }, diff --git a/src/renderer/src/windows/mini/App.tsx b/src/renderer/src/windows/mini/App.tsx index 41f06b8d..416ba11b 100644 --- a/src/renderer/src/windows/mini/App.tsx +++ b/src/renderer/src/windows/mini/App.tsx @@ -1,7 +1,10 @@ import '@renderer/databases' -import store, { persistor } from '@renderer/store' +import { useSettings } from '@renderer/hooks/useSettings' +import store, { persistor, useAppDispatch } from '@renderer/store' import { message } from 'antd' +import { setCustomCss } from '@renderer/store/settings' +import { useEffect, useState } from 'react' import { Provider } from 'react-redux' import { PersistGate } from 'redux-persist/integration/react' @@ -10,6 +13,50 @@ import { SyntaxHighlighterProvider } from '../../context/SyntaxHighlighterProvid import { ThemeProvider } from '../../context/ThemeProvider' import HomeWindow from './home/HomeWindow' +function useMiniWindowCustomCss() { + const { customCss } = useSettings() + const dispatch = useAppDispatch() + const [isInitialized, setIsInitialized] = useState(false) + + useEffect(() => { + // 初始化时从主进程获取最新的CSS配置 + window.api.config.get('customCss').then((css) => { + if (css !== undefined) { + dispatch(setCustomCss(css)) + } + setIsInitialized(true) + }) + + // Listen for custom CSS updates from main window + const removeListener = window.electron.ipcRenderer.on('custom-css:update', (_event, css) => { + dispatch(setCustomCss(css)) + }) + + return () => { + removeListener() + } + }, [dispatch]) + + useEffect(() => { + if (!isInitialized) return + + // Apply custom CSS + const oldCustomCss = document.getElementById('user-defined-custom-css') + if (oldCustomCss) { + oldCustomCss.remove() + } + + if (customCss) { + const style = document.createElement('style') + style.id = 'user-defined-custom-css' + style.textContent = customCss + document.head.appendChild(style) + } + }, [customCss, isInitialized]) + + return isInitialized +} + function MiniWindow(): React.ReactElement { //miniWindow should register its own message component const [messageApi, messageContextHolder] = message.useMessage() @@ -22,7 +69,7 @@ function MiniWindow(): React.ReactElement { {messageContextHolder} - + @@ -31,4 +78,16 @@ function MiniWindow(): React.ReactElement { ) } +// Inner component that uses the hook after Redux is initialized +function MiniWindowContent(): React.ReactElement { + const cssInitialized = useMiniWindowCustomCss() + + // Show empty fragment until CSS is initialized + if (!cssInitialized) { + return <> + } + + return +} + export default MiniWindow