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

View File

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

View File

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

View File

@ -19,6 +19,7 @@ import {
MessageSquareQuote,
Moon,
Palette,
RefreshCcw,
Settings,
Sparkle,
Sun
@ -98,7 +99,13 @@ const Sidebar: FC = () => {
mouseEnterDelay={0.8}
placement="right">
<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>
</Tooltip>
<Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right">

View File

@ -11,8 +11,8 @@ interface ThemeContextType {
}
const ThemeContext = createContext<ThemeContextType>({
theme: ThemeMode.light,
settingTheme: ThemeMode.light,
theme: ThemeMode.auto,
settingTheme: ThemeMode.auto,
toggleTheme: () => {}
})
@ -22,43 +22,37 @@ interface ThemeProviderProps extends PropsWithChildren {
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children, defaultTheme }) => {
const { theme, setTheme } = useSettings()
const [_theme, _setTheme] = useState(theme)
const [effectiveTheme, setEffectiveTheme] = useState(theme)
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 => {
if (theme === ThemeMode.auto || defaultTheme === ThemeMode.auto) {
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)
}
useEffect(() => {
window.api?.setTheme(defaultTheme || theme)
}, [defaultTheme, theme])
useEffect(() => {
document.body.setAttribute('theme-mode', _theme)
// 移除迷你窗口的条件判断,让所有窗口都能设置主题
window.api?.setTheme(_theme === ThemeMode.dark ? 'dark' : 'light')
}, [_theme])
document.body.setAttribute('theme-mode', effectiveTheme)
}, [effectiveTheme])
useEffect(() => {
document.body.setAttribute('os', isMac ? 'mac' : 'windows')
// listen theme change from main process from other windows
const themeChangeListenerRemover = window.electron.ipcRenderer.on(IpcChannel.ThemeChange, (_, newTheme) => {
setTheme(newTheme)
})
const themeChangeListenerRemover = window.electron.ipcRenderer.on(
IpcChannel.ThemeChange,
(_, realTheam: ThemeMode) => {
setEffectiveTheme(realTheam)
}
)
return () => {
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)