feat: Enhance theme management with auto mode support (#5374)

- Updated IPC channel to handle 'auto' theme mode, allowing dynamic theme changes based on system preferences.
- Modified theme setting functions in preload scripts to accommodate the new 'auto' option.
- Adjusted Sidebar component to display an icon for 'auto' theme mode.
- Refactored ThemeProvider to manage theme state more effectively and listen for theme changes across windows.
This commit is contained in:
beyondkmp 2025-04-26 22:16:12 +08:00 committed by GitHub
parent 6b113c19a3
commit e5e04c8132
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 47 additions and 42 deletions

View File

@ -5,7 +5,7 @@ import { isMac, isWin } from '@main/constant'
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process' import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
import { IpcChannel } from '@shared/IpcChannel' import { IpcChannel } from '@shared/IpcChannel'
import { Shortcut, ThemeMode } from '@types' import { Shortcut, ThemeMode } from '@types'
import { BrowserWindow, ipcMain, session, shell } from 'electron' import { BrowserWindow, ipcMain, nativeTheme, session, shell } from 'electron'
import log from 'electron-log' import log from 'electron-log'
import { titleBarOverlayDark, titleBarOverlayLight } from './config' import { titleBarOverlayDark, titleBarOverlayLight } from './config'
@ -119,23 +119,26 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
}) })
// theme // theme
ipcMain.handle(IpcChannel.App_SetTheme, (event, theme: ThemeMode) => { ipcMain.handle(IpcChannel.App_SetTheme, (_, theme: ThemeMode) => {
if (theme === configManager.getTheme()) return const notifyThemeChange = () => {
const windows = BrowserWindow.getAllWindows()
windows.forEach((win) =>
win.webContents.send(IpcChannel.ThemeChange, nativeTheme.shouldUseDarkColors ? ThemeMode.dark : ThemeMode.light)
)
}
configManager.setTheme(theme) if (theme === ThemeMode.auto) {
nativeTheme.themeSource = 'system'
// should sync theme change to all windows nativeTheme.on('updated', notifyThemeChange)
const senderWindowId = event.sender.id } else {
const windows = BrowserWindow.getAllWindows() nativeTheme.themeSource = theme
// 向其他窗口广播主题变化 nativeTheme.removeAllListeners('updated')
windows.forEach((win) => { }
if (win.webContents.id !== senderWindowId) {
win.webContents.send(IpcChannel.ThemeChange, theme)
}
})
mainWindow?.setTitleBarOverlay && mainWindow?.setTitleBarOverlay &&
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight) mainWindow.setTitleBarOverlay(nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight)
configManager.setTheme(theme)
notifyThemeChange()
}) })
// custom css // custom css

View File

@ -28,7 +28,8 @@ declare global {
setTray: (isActive: boolean) => void setTray: (isActive: boolean) => void
setTrayOnClose: (isActive: boolean) => void setTrayOnClose: (isActive: boolean) => void
restartTray: () => void restartTray: () => void
setTheme: (theme: 'light' | 'dark') => void setTheme: (theme: 'light' | 'dark' | 'auto') => void
getTheme: () => Promise<'light' | 'dark' | 'auto'>
setCustomCss: (css: string) => void setCustomCss: (css: string) => void
setAutoUpdate: (isActive: boolean) => void setAutoUpdate: (isActive: boolean) => void
reload: () => void reload: () => void

View File

@ -18,7 +18,7 @@ const api = {
setTray: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTray, isActive), setTray: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTray, isActive),
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' | 'auto') => ipcRenderer.invoke(IpcChannel.App_SetTheme, theme),
setCustomCss: (css: string) => ipcRenderer.invoke(IpcChannel.App_SetCustomCss, css), setCustomCss: (css: string) => ipcRenderer.invoke(IpcChannel.App_SetCustomCss, css),
setAutoUpdate: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetAutoUpdate, isActive), setAutoUpdate: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetAutoUpdate, isActive),
openWebsite: (url: string) => ipcRenderer.invoke(IpcChannel.Open_Website, url), openWebsite: (url: string) => ipcRenderer.invoke(IpcChannel.Open_Website, url),

View File

@ -19,6 +19,7 @@ import {
MessageSquareQuote, MessageSquareQuote,
Moon, Moon,
Palette, Palette,
RefreshCcw,
Settings, Settings,
Sparkle, Sparkle,
Sun Sun
@ -98,7 +99,13 @@ const Sidebar: FC = () => {
mouseEnterDelay={0.8} mouseEnterDelay={0.8}
placement="right"> placement="right">
<Icon theme={theme} onClick={() => toggleTheme()}> <Icon theme={theme} onClick={() => toggleTheme()}>
{theme === 'dark' ? <Moon size={20} className="icon" /> : <Sun size={20} className="icon" />} {settingTheme === 'dark' ? (
<Moon size={20} className="icon" />
) : settingTheme === 'light' ? (
<Sun size={20} className="icon" />
) : (
<RefreshCcw size={20} className="icon" />
)}
</Icon> </Icon>
</Tooltip> </Tooltip>
<Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right"> <Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right">

View File

@ -11,8 +11,8 @@ interface ThemeContextType {
} }
const ThemeContext = createContext<ThemeContextType>({ const ThemeContext = createContext<ThemeContextType>({
theme: ThemeMode.light, theme: ThemeMode.auto,
settingTheme: ThemeMode.light, settingTheme: ThemeMode.auto,
toggleTheme: () => {} toggleTheme: () => {}
}) })
@ -22,43 +22,37 @@ interface ThemeProviderProps extends PropsWithChildren {
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children, defaultTheme }) => { export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children, defaultTheme }) => {
const { theme, setTheme } = useSettings() const { theme, setTheme } = useSettings()
const [_theme, _setTheme] = useState(theme) const [effectiveTheme, setEffectiveTheme] = useState(theme)
const toggleTheme = () => { const toggleTheme = () => {
setTheme(theme === ThemeMode.dark ? ThemeMode.light : ThemeMode.dark) // 主题顺序是light, dark, auto, 所以需要先判断当前主题,然后取下一个主题
const nextTheme =
theme === ThemeMode.light ? ThemeMode.dark : theme === ThemeMode.dark ? ThemeMode.auto : ThemeMode.light
setTheme(nextTheme)
} }
useEffect((): any => { useEffect(() => {
if (theme === ThemeMode.auto || defaultTheme === ThemeMode.auto) { window.api?.setTheme(defaultTheme || theme)
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
_setTheme(mediaQuery.matches ? ThemeMode.dark : ThemeMode.light)
const handleChange = (e: MediaQueryListEvent) => _setTheme(e.matches ? ThemeMode.dark : ThemeMode.light)
mediaQuery.addEventListener('change', handleChange)
return () => mediaQuery.removeEventListener('change', handleChange)
} else {
_setTheme(theme)
}
}, [defaultTheme, theme]) }, [defaultTheme, theme])
useEffect(() => { useEffect(() => {
document.body.setAttribute('theme-mode', _theme) document.body.setAttribute('theme-mode', effectiveTheme)
// 移除迷你窗口的条件判断,让所有窗口都能设置主题 }, [effectiveTheme])
window.api?.setTheme(_theme === ThemeMode.dark ? 'dark' : 'light')
}, [_theme])
useEffect(() => { useEffect(() => {
document.body.setAttribute('os', isMac ? 'mac' : 'windows') document.body.setAttribute('os', isMac ? 'mac' : 'windows')
const themeChangeListenerRemover = window.electron.ipcRenderer.on(
// listen theme change from main process from other windows IpcChannel.ThemeChange,
const themeChangeListenerRemover = window.electron.ipcRenderer.on(IpcChannel.ThemeChange, (_, newTheme) => { (_, realTheam: ThemeMode) => {
setTheme(newTheme) setEffectiveTheme(realTheam)
}) }
)
return () => { return () => {
themeChangeListenerRemover() themeChangeListenerRemover()
} }
}) })
return <ThemeContext value={{ theme: _theme, settingTheme: theme, toggleTheme }}>{children}</ThemeContext> return <ThemeContext value={{ theme: effectiveTheme, settingTheme: theme, toggleTheme }}>{children}</ThemeContext>
} }
export const useTheme = () => use(ThemeContext) export const useTheme = () => use(ThemeContext)