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:
parent
c5580f5b71
commit
4fa04a801a
@ -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',
|
||||||
|
|||||||
@ -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')]
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/preload/index.d.ts
vendored
1
src/preload/index.d.ts
vendored
@ -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: {
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user