mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-23 01:50:13 +08:00
feat(theme): 用户自定义主题色 (#4613)
* feat(theme): 用户自定义主题色 * refactor(QuickPanel): integrate user theme for dynamic color handling * refactor(ThemeProvider): separate user theme initialization into its own useEffect * refactor(useUserTheme): move theme initialization logic into a dedicated function * feat(settings): enhance color picker with presets and update styles for ant-collapse * feat: Refactor theme management to use userTheme object for colorPrimary
This commit is contained in:
parent
2e8cbdc4aa
commit
c5208eeaef
@ -206,8 +206,14 @@
|
|||||||
|
|
||||||
.ant-collapse {
|
.ant-collapse {
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
|
.ant-color-picker & {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-collapse-content {
|
.ant-collapse-content {
|
||||||
border-top: 1px solid var(--color-border) !important;
|
border-top: 1px solid var(--color-border) !important;
|
||||||
|
.ant-color-picker & {
|
||||||
|
border-top: none !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import { RightOutlined } from '@ant-design/icons'
|
import { RightOutlined } from '@ant-design/icons'
|
||||||
import { isMac } from '@renderer/config/constant'
|
import { isMac } from '@renderer/config/constant'
|
||||||
|
import useUserTheme from '@renderer/hooks/useUserTheme'
|
||||||
import { classNames } from '@renderer/utils'
|
import { classNames } from '@renderer/utils'
|
||||||
import { Flex } from 'antd'
|
import { Flex } from 'antd'
|
||||||
import { theme } from 'antd'
|
|
||||||
import Color from 'color'
|
|
||||||
import { t } from 'i18next'
|
import { t } from 'i18next'
|
||||||
import { Check } from 'lucide-react'
|
import { Check } from 'lucide-react'
|
||||||
import React, { use, useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
import React, { use, useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||||
@ -40,8 +39,7 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
|
|||||||
throw new Error('QuickPanel must be used within a QuickPanelProvider')
|
throw new Error('QuickPanel must be used within a QuickPanelProvider')
|
||||||
}
|
}
|
||||||
|
|
||||||
const { token } = theme.useToken()
|
const { colorPrimary } = useUserTheme()
|
||||||
const colorPrimary = Color(token.colorPrimary || '#008000')
|
|
||||||
const selectedColor = colorPrimary.alpha(0.15).toString()
|
const selectedColor = colorPrimary.alpha(0.15).toString()
|
||||||
const selectedColorHover = colorPrimary.alpha(0.2).toString()
|
const selectedColorHover = colorPrimary.alpha(0.2).toString()
|
||||||
|
|
||||||
|
|||||||
@ -15,13 +15,18 @@ import { FC, PropsWithChildren } from 'react'
|
|||||||
import { useTheme } from './ThemeProvider'
|
import { useTheme } from './ThemeProvider'
|
||||||
|
|
||||||
const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
|
const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||||
const { language } = useSettings()
|
const {
|
||||||
|
language,
|
||||||
|
userTheme: { colorPrimary }
|
||||||
|
} = useSettings()
|
||||||
const { theme: _theme } = useTheme()
|
const { theme: _theme } = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
locale={getAntdLocale(language)}
|
locale={getAntdLocale(language)}
|
||||||
theme={{
|
theme={{
|
||||||
|
cssVar: true,
|
||||||
|
hashed: false,
|
||||||
algorithm: [_theme === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm],
|
algorithm: [_theme === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm],
|
||||||
components: {
|
components: {
|
||||||
Menu: {
|
Menu: {
|
||||||
@ -43,7 +48,7 @@ const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
token: {
|
token: {
|
||||||
colorPrimary: '#00b96b',
|
colorPrimary: colorPrimary,
|
||||||
fontFamily: 'var(--font-family)'
|
fontFamily: 'var(--font-family)'
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { isMac } from '@renderer/config/constant'
|
import { isMac } from '@renderer/config/constant'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import useUserTheme from '@renderer/hooks/useUserTheme'
|
||||||
import { ThemeMode } from '@renderer/types'
|
import { ThemeMode } from '@renderer/types'
|
||||||
import { IpcChannel } from '@shared/IpcChannel'
|
import { IpcChannel } from '@shared/IpcChannel'
|
||||||
import React, { createContext, PropsWithChildren, use, useEffect, useState } from 'react'
|
import React, { createContext, PropsWithChildren, use, useEffect, useState } from 'react'
|
||||||
@ -23,6 +24,7 @@ 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 [effectiveTheme, setEffectiveTheme] = useState(theme)
|
const [effectiveTheme, setEffectiveTheme] = useState(theme)
|
||||||
|
const { initUserTheme } = useUserTheme()
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
// 主题顺序是light, dark, auto, 所以需要先判断当前主题,然后取下一个主题
|
// 主题顺序是light, dark, auto, 所以需要先判断当前主题,然后取下一个主题
|
||||||
@ -60,6 +62,11 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children, defaultT
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initUserTheme()
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
return <ThemeContext value={{ theme: effectiveTheme, settingTheme: theme, toggleTheme }}>{children}</ThemeContext>
|
return <ThemeContext value={{ theme: effectiveTheme, settingTheme: theme, toggleTheme }}>{children}</ThemeContext>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
28
src/renderer/src/hooks/useUserTheme.ts
Normal file
28
src/renderer/src/hooks/useUserTheme.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
|
import { setUserTheme, UserTheme } from '@renderer/store/settings'
|
||||||
|
import Color from 'color'
|
||||||
|
|
||||||
|
export default function useUserTheme() {
|
||||||
|
const userTheme = useAppSelector((state) => state.settings.userTheme)
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
const initUserTheme = (theme: UserTheme = userTheme) => {
|
||||||
|
const colorPrimary = Color(theme.colorPrimary)
|
||||||
|
|
||||||
|
document.body.style.setProperty('--color-primary', colorPrimary.toString())
|
||||||
|
document.body.style.setProperty('--color-primary-soft', colorPrimary.alpha(0.6).toString())
|
||||||
|
document.body.style.setProperty('--color-primary-mute', colorPrimary.alpha(0.3).toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
colorPrimary: Color(userTheme.colorPrimary),
|
||||||
|
|
||||||
|
initUserTheme,
|
||||||
|
|
||||||
|
setUserTheme(userTheme: UserTheme) {
|
||||||
|
dispatch(setUserTheme(userTheme))
|
||||||
|
|
||||||
|
initUserTheme(userTheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1695,6 +1695,7 @@
|
|||||||
"theme.dark": "Dark",
|
"theme.dark": "Dark",
|
||||||
"theme.light": "Light",
|
"theme.light": "Light",
|
||||||
"theme.title": "Theme",
|
"theme.title": "Theme",
|
||||||
|
"theme.color_primary": "Primary Color",
|
||||||
"theme.window.style.opaque": "Opaque Window",
|
"theme.window.style.opaque": "Opaque Window",
|
||||||
"theme.window.style.title": "Window Style",
|
"theme.window.style.title": "Window Style",
|
||||||
"theme.window.style.transparent": "Transparent Window",
|
"theme.window.style.transparent": "Transparent Window",
|
||||||
|
|||||||
@ -1685,6 +1685,7 @@
|
|||||||
"theme.dark": "ダーク",
|
"theme.dark": "ダーク",
|
||||||
"theme.light": "ライト",
|
"theme.light": "ライト",
|
||||||
"theme.title": "テーマ",
|
"theme.title": "テーマ",
|
||||||
|
"theme.color_primary": "テーマ色",
|
||||||
"theme.window.style.opaque": "不透明ウィンドウ",
|
"theme.window.style.opaque": "不透明ウィンドウ",
|
||||||
"theme.window.style.title": "ウィンドウスタイル",
|
"theme.window.style.title": "ウィンドウスタイル",
|
||||||
"theme.window.style.transparent": "透明ウィンドウ",
|
"theme.window.style.transparent": "透明ウィンドウ",
|
||||||
|
|||||||
@ -1686,6 +1686,7 @@
|
|||||||
"theme.dark": "Темная",
|
"theme.dark": "Темная",
|
||||||
"theme.light": "Светлая",
|
"theme.light": "Светлая",
|
||||||
"theme.title": "Тема",
|
"theme.title": "Тема",
|
||||||
|
"theme.color_primary": "Цвет темы",
|
||||||
"theme.window.style.opaque": "Непрозрачное окно",
|
"theme.window.style.opaque": "Непрозрачное окно",
|
||||||
"theme.window.style.title": "Стиль окна",
|
"theme.window.style.title": "Стиль окна",
|
||||||
"theme.window.style.transparent": "Прозрачное окно",
|
"theme.window.style.transparent": "Прозрачное окно",
|
||||||
|
|||||||
@ -1695,6 +1695,7 @@
|
|||||||
"theme.dark": "深色",
|
"theme.dark": "深色",
|
||||||
"theme.light": "浅色",
|
"theme.light": "浅色",
|
||||||
"theme.title": "主题",
|
"theme.title": "主题",
|
||||||
|
"theme.color_primary": "主题颜色",
|
||||||
"theme.window.style.opaque": "不透明窗口",
|
"theme.window.style.opaque": "不透明窗口",
|
||||||
"theme.window.style.title": "窗口样式",
|
"theme.window.style.title": "窗口样式",
|
||||||
"theme.window.style.transparent": "透明窗口",
|
"theme.window.style.transparent": "透明窗口",
|
||||||
|
|||||||
@ -1688,6 +1688,7 @@
|
|||||||
"theme.dark": "深色",
|
"theme.dark": "深色",
|
||||||
"theme.light": "淺色",
|
"theme.light": "淺色",
|
||||||
"theme.title": "主題",
|
"theme.title": "主題",
|
||||||
|
"theme.color_primary": "主題顏色",
|
||||||
"theme.window.style.opaque": "不透明視窗",
|
"theme.window.style.opaque": "不透明視窗",
|
||||||
"theme.window.style.title": "視窗樣式",
|
"theme.window.style.title": "視窗樣式",
|
||||||
"theme.window.style.transparent": "透明視窗",
|
"theme.window.style.transparent": "透明視窗",
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { SyncOutlined } from '@ant-design/icons'
|
|||||||
import { isMac } from '@renderer/config/constant'
|
import { isMac } from '@renderer/config/constant'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import useUserTheme from '@renderer/hooks/useUserTheme'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import {
|
import {
|
||||||
AssistantIconType,
|
AssistantIconType,
|
||||||
@ -14,9 +15,9 @@ import {
|
|||||||
setSidebarIcons
|
setSidebarIcons
|
||||||
} from '@renderer/store/settings'
|
} from '@renderer/store/settings'
|
||||||
import { ThemeMode } from '@renderer/types'
|
import { ThemeMode } from '@renderer/types'
|
||||||
import { Button, Input, Segmented, Switch } from 'antd'
|
|
||||||
import { Minus, Plus, RotateCcw } from 'lucide-react'
|
import { Minus, Plus, RotateCcw } from 'lucide-react'
|
||||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
|
import { Button, ColorPicker, Input, Segmented, Switch } from 'antd'
|
||||||
|
import { FC, useCallback, useMemo, useState, useEffect } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@ -36,12 +37,14 @@ const DisplaySettings: FC = () => {
|
|||||||
pinTopicsToTop,
|
pinTopicsToTop,
|
||||||
customCss,
|
customCss,
|
||||||
sidebarIcons,
|
sidebarIcons,
|
||||||
assistantIconType
|
assistantIconType,
|
||||||
|
userTheme
|
||||||
} = useSettings()
|
} = useSettings()
|
||||||
const { theme: themeMode } = useTheme()
|
const { theme: themeMode } = useTheme()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const [currentZoom, setCurrentZoom] = useState(1.0)
|
const [currentZoom, setCurrentZoom] = useState(1.0)
|
||||||
|
const { setUserTheme } = useUserTheme()
|
||||||
|
|
||||||
const [visibleIcons, setVisibleIcons] = useState(sidebarIcons?.visible || DEFAULT_SIDEBAR_ICONS)
|
const [visibleIcons, setVisibleIcons] = useState(sidebarIcons?.visible || DEFAULT_SIDEBAR_ICONS)
|
||||||
const [disabledIcons, setDisabledIcons] = useState(sidebarIcons?.disabled || [])
|
const [disabledIcons, setDisabledIcons] = useState(sidebarIcons?.disabled || [])
|
||||||
@ -53,6 +56,16 @@ const DisplaySettings: FC = () => {
|
|||||||
[setWindowStyle]
|
[setWindowStyle]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleColorPrimaryChange = useCallback(
|
||||||
|
(colorHex: string) => {
|
||||||
|
setUserTheme({
|
||||||
|
...userTheme,
|
||||||
|
colorPrimary: colorHex
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[setUserTheme, userTheme]
|
||||||
|
)
|
||||||
|
|
||||||
const handleReset = useCallback(() => {
|
const handleReset = useCallback(() => {
|
||||||
setVisibleIcons([...DEFAULT_SIDEBAR_ICONS])
|
setVisibleIcons([...DEFAULT_SIDEBAR_ICONS])
|
||||||
setDisabledIcons([])
|
setDisabledIcons([])
|
||||||
@ -149,6 +162,22 @@ const DisplaySettings: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</ZoomButtonGroup>
|
</ZoomButtonGroup>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.theme.color_primary')}</SettingRowTitle>
|
||||||
|
<ColorPicker
|
||||||
|
className="color-picker"
|
||||||
|
value={userTheme.colorPrimary}
|
||||||
|
onChange={(color) => handleColorPrimaryChange(color.toHexString())}
|
||||||
|
showText
|
||||||
|
presets={[
|
||||||
|
{
|
||||||
|
label: 'Presets',
|
||||||
|
colors: ['#007BFF', '#F74F9E', '#FF5257', '#F7821B', '#FFC600', '#62BA46', '#000000']
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
{isMac && (
|
{isMac && (
|
||||||
<>
|
<>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
|
|||||||
@ -1465,6 +1465,14 @@ const migrateConfig = {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
'109': (state: RootState) => {
|
||||||
|
try {
|
||||||
|
state.settings.userTheme = settingsInitialState.userTheme
|
||||||
|
return state
|
||||||
|
} catch (error) {
|
||||||
|
return state
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -31,6 +31,10 @@ export interface NutstoreSyncRuntime extends WebDAVSyncState {}
|
|||||||
|
|
||||||
export type AssistantIconType = 'model' | 'emoji' | 'none'
|
export type AssistantIconType = 'model' | 'emoji' | 'none'
|
||||||
|
|
||||||
|
export type UserTheme = {
|
||||||
|
colorPrimary: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface SettingsState {
|
export interface SettingsState {
|
||||||
showAssistants: boolean
|
showAssistants: boolean
|
||||||
showTopics: boolean
|
showTopics: boolean
|
||||||
@ -49,6 +53,7 @@ export interface SettingsState {
|
|||||||
trayOnClose: boolean
|
trayOnClose: boolean
|
||||||
tray: boolean
|
tray: boolean
|
||||||
theme: ThemeMode
|
theme: ThemeMode
|
||||||
|
userTheme: UserTheme
|
||||||
windowStyle: 'transparent' | 'opaque'
|
windowStyle: 'transparent' | 'opaque'
|
||||||
fontSize: number
|
fontSize: number
|
||||||
topicPosition: 'left' | 'right'
|
topicPosition: 'left' | 'right'
|
||||||
@ -191,6 +196,9 @@ export const initialState: SettingsState = {
|
|||||||
trayOnClose: true,
|
trayOnClose: true,
|
||||||
tray: true,
|
tray: true,
|
||||||
theme: ThemeMode.auto,
|
theme: ThemeMode.auto,
|
||||||
|
userTheme: {
|
||||||
|
colorPrimary: '#00b96b'
|
||||||
|
},
|
||||||
windowStyle: 'opaque',
|
windowStyle: 'opaque',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
topicPosition: 'left',
|
topicPosition: 'left',
|
||||||
@ -368,6 +376,9 @@ const settingsSlice = createSlice({
|
|||||||
setCustomCss: (state, action: PayloadAction<string>) => {
|
setCustomCss: (state, action: PayloadAction<string>) => {
|
||||||
state.customCss = action.payload
|
state.customCss = action.payload
|
||||||
},
|
},
|
||||||
|
setUserTheme: (state, action: PayloadAction<UserTheme>) => {
|
||||||
|
state.userTheme = action.payload
|
||||||
|
},
|
||||||
setFontSize: (state, action: PayloadAction<number>) => {
|
setFontSize: (state, action: PayloadAction<number>) => {
|
||||||
state.fontSize = action.payload
|
state.fontSize = action.payload
|
||||||
},
|
},
|
||||||
@ -662,6 +673,7 @@ export const {
|
|||||||
setTrayOnClose,
|
setTrayOnClose,
|
||||||
setTray,
|
setTray,
|
||||||
setTheme,
|
setTheme,
|
||||||
|
setUserTheme,
|
||||||
setFontSize,
|
setFontSize,
|
||||||
setWindowStyle,
|
setWindowStyle,
|
||||||
setTopicPosition,
|
setTopicPosition,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user