mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-09 06:49:02 +08:00
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:
parent
6b113c19a3
commit
e5e04c8132
@ -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
|
||||||
|
|||||||
3
src/preload/index.d.ts
vendored
3
src/preload/index.d.ts
vendored
@ -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
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user