feat(UI): Support custcom css in mini window (#4255)

* feat(UI): enable custom CSS functionality with miniWindow

* feat(UI): implement custom CSS handling in IPC and update related components
This commit is contained in:
SuYao 2025-04-17 20:54:34 +08:00 committed by GitHub
parent c5580f5b71
commit 4fa04a801a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 97 additions and 8 deletions

View File

@ -12,6 +12,7 @@ export enum IpcChannel {
App_SetTrayOnClose = 'app:set-tray-on-close', App_SetTrayOnClose = 'app:set-tray-on-close',
App_RestartTray = 'app:restart-tray', App_RestartTray = 'app:restart-tray',
App_SetTheme = 'app:set-theme', App_SetTheme = 'app:set-theme',
App_SetCustomCss = 'app:set-custom-css',
App_IsBinaryExist = 'app:is-binary-exist', App_IsBinaryExist = 'app:is-binary-exist',
App_GetBinaryPath = 'app:get-binary-path', App_GetBinaryPath = 'app:get-binary-path',

View File

@ -130,6 +130,22 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight) 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 // clear cache
ipcMain.handle(IpcChannel.App_ClearCache, async () => { ipcMain.handle(IpcChannel.App_ClearCache, async () => {
const sessions = [session.defaultSession, session.fromPartition('persist:webview')] const sessions = [session.defaultSession, session.fromPartition('persist:webview')]

View File

@ -42,6 +42,14 @@ export class ConfigManager {
this.set(ConfigKeys.Theme, theme) this.set(ConfigKeys.Theme, theme)
} }
getCustomCss(): string {
return this.store.get('customCss', '') as string
}
setCustomCss(css: string) {
this.store.set('customCss', css)
}
getLaunchToTray(): boolean { getLaunchToTray(): boolean {
return !!this.get(ConfigKeys.LaunchToTray, false) return !!this.get(ConfigKeys.LaunchToTray, false)
} }

View File

@ -29,6 +29,7 @@ declare global {
setTrayOnClose: (isActive: boolean) => void setTrayOnClose: (isActive: boolean) => void
restartTray: () => void restartTray: () => void
setTheme: (theme: 'light' | 'dark') => void setTheme: (theme: 'light' | 'dark') => void
setCustomCss: (css: string) => void
reload: () => void reload: () => void
clearCache: () => Promise<{ success: boolean; error?: string }> clearCache: () => Promise<{ success: boolean; error?: string }>
system: { system: {

View File

@ -19,6 +19,7 @@ const api = {
setTrayOnClose: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTrayOnClose, isActive), setTrayOnClose: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTrayOnClose, isActive),
restartTray: () => ipcRenderer.invoke(IpcChannel.App_RestartTray), restartTray: () => ipcRenderer.invoke(IpcChannel.App_RestartTray),
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke(IpcChannel.App_SetTheme, theme), 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), openWebsite: (url: string) => ipcRenderer.invoke(IpcChannel.Open_Website, url),
clearCache: () => ipcRenderer.invoke(IpcChannel.App_ClearCache), clearCache: () => ipcRenderer.invoke(IpcChannel.App_ClearCache),
system: { system: {

View File

@ -331,7 +331,7 @@ const MinappPopupContainer: React.FC = () => {
height={'100%'} height={'100%'}
maskClosable={false} maskClosable={false}
closeIcon={null} closeIcon={null}
style={{ marginLeft: 'var(--sidebar-width)' }}> style={{ marginLeft: 'var(--sidebar-width)', backgroundColor: 'var(--color-background)' }}>
{!isReady && ( {!isReady && (
<EmptyView> <EmptyView>
<Avatar <Avatar

View File

@ -75,7 +75,7 @@ const WebviewContainer = memo(
const WebviewStyle: React.CSSProperties = { const WebviewStyle: React.CSSProperties = {
width: 'calc(100vw - var(--sidebar-width))', width: 'calc(100vw - var(--sidebar-width))',
height: 'calc(100vh - var(--navbar-height))', height: 'calc(100vh - var(--navbar-height))',
backgroundColor: 'white', backgroundColor: 'var(--color-background)',
display: 'inline-flex' display: 'inline-flex'
} }

View File

@ -188,7 +188,10 @@ const DisplaySettings: FC = () => {
<SettingDivider /> <SettingDivider />
<Input.TextArea <Input.TextArea
value={customCss} value={customCss}
onChange={(e) => 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')} placeholder={t('settings.display.custom.css.placeholder')}
style={{ style={{
minHeight: 200, minHeight: 200,

View File

@ -286,6 +286,9 @@ const settingsSlice = createSlice({
setTheme: (state, action: PayloadAction<ThemeMode>) => { setTheme: (state, action: PayloadAction<ThemeMode>) => {
state.theme = action.payload state.theme = action.payload
}, },
setCustomCss: (state, action: PayloadAction<string>) => {
state.customCss = action.payload
},
setFontSize: (state, action: PayloadAction<number>) => { setFontSize: (state, action: PayloadAction<number>) => {
state.fontSize = action.payload state.fontSize = action.payload
}, },
@ -382,9 +385,6 @@ const settingsSlice = createSlice({
setPasteLongTextThreshold: (state, action: PayloadAction<number>) => { setPasteLongTextThreshold: (state, action: PayloadAction<number>) => {
state.pasteLongTextThreshold = action.payload state.pasteLongTextThreshold = action.payload
}, },
setCustomCss: (state, action: PayloadAction<string>) => {
state.customCss = action.payload
},
setTopicNamingPrompt: (state, action: PayloadAction<string>) => { setTopicNamingPrompt: (state, action: PayloadAction<string>) => {
state.topicNamingPrompt = action.payload state.topicNamingPrompt = action.payload
}, },

View File

@ -1,7 +1,10 @@
import '@renderer/databases' 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 { message } from 'antd'
import { setCustomCss } from '@renderer/store/settings'
import { useEffect, useState } from 'react'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'
@ -10,6 +13,50 @@ import { SyntaxHighlighterProvider } from '../../context/SyntaxHighlighterProvid
import { ThemeProvider } from '../../context/ThemeProvider' import { ThemeProvider } from '../../context/ThemeProvider'
import HomeWindow from './home/HomeWindow' 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 { function MiniWindow(): React.ReactElement {
//miniWindow should register its own message component //miniWindow should register its own message component
const [messageApi, messageContextHolder] = message.useMessage() const [messageApi, messageContextHolder] = message.useMessage()
@ -22,7 +69,7 @@ function MiniWindow(): React.ReactElement {
<SyntaxHighlighterProvider> <SyntaxHighlighterProvider>
<PersistGate loading={null} persistor={persistor}> <PersistGate loading={null} persistor={persistor}>
{messageContextHolder} {messageContextHolder}
<HomeWindow /> <MiniWindowContent />
</PersistGate> </PersistGate>
</SyntaxHighlighterProvider> </SyntaxHighlighterProvider>
</AntdProvider> </AntdProvider>
@ -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 <HomeWindow />
}
export default MiniWindow export default MiniWindow