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 {
|
||||
border: 1px solid var(--color-border);
|
||||
.ant-color-picker & {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
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 { isMac } from '@renderer/config/constant'
|
||||
import useUserTheme from '@renderer/hooks/useUserTheme'
|
||||
import { classNames } from '@renderer/utils'
|
||||
import { Flex } from 'antd'
|
||||
import { theme } from 'antd'
|
||||
import Color from 'color'
|
||||
import { t } from 'i18next'
|
||||
import { Check } from 'lucide-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')
|
||||
}
|
||||
|
||||
const { token } = theme.useToken()
|
||||
const colorPrimary = Color(token.colorPrimary || '#008000')
|
||||
const { colorPrimary } = useUserTheme()
|
||||
const selectedColor = colorPrimary.alpha(0.15).toString()
|
||||
const selectedColorHover = colorPrimary.alpha(0.2).toString()
|
||||
|
||||
|
||||
@ -15,13 +15,18 @@ import { FC, PropsWithChildren } from 'react'
|
||||
import { useTheme } from './ThemeProvider'
|
||||
|
||||
const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
const { language } = useSettings()
|
||||
const {
|
||||
language,
|
||||
userTheme: { colorPrimary }
|
||||
} = useSettings()
|
||||
const { theme: _theme } = useTheme()
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
locale={getAntdLocale(language)}
|
||||
theme={{
|
||||
cssVar: true,
|
||||
hashed: false,
|
||||
algorithm: [_theme === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm],
|
||||
components: {
|
||||
Menu: {
|
||||
@ -43,7 +48,7 @@ const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
}
|
||||
},
|
||||
token: {
|
||||
colorPrimary: '#00b96b',
|
||||
colorPrimary: colorPrimary,
|
||||
fontFamily: 'var(--font-family)'
|
||||
}
|
||||
}}>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import useUserTheme from '@renderer/hooks/useUserTheme'
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
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 }) => {
|
||||
const { theme, setTheme } = useSettings()
|
||||
const [effectiveTheme, setEffectiveTheme] = useState(theme)
|
||||
const { initUserTheme } = useUserTheme()
|
||||
|
||||
const toggleTheme = () => {
|
||||
// 主题顺序是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>
|
||||
}
|
||||
|
||||
|
||||
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.light": "Light",
|
||||
"theme.title": "Theme",
|
||||
"theme.color_primary": "Primary Color",
|
||||
"theme.window.style.opaque": "Opaque Window",
|
||||
"theme.window.style.title": "Window Style",
|
||||
"theme.window.style.transparent": "Transparent Window",
|
||||
|
||||
@ -1685,6 +1685,7 @@
|
||||
"theme.dark": "ダーク",
|
||||
"theme.light": "ライト",
|
||||
"theme.title": "テーマ",
|
||||
"theme.color_primary": "テーマ色",
|
||||
"theme.window.style.opaque": "不透明ウィンドウ",
|
||||
"theme.window.style.title": "ウィンドウスタイル",
|
||||
"theme.window.style.transparent": "透明ウィンドウ",
|
||||
|
||||
@ -1686,6 +1686,7 @@
|
||||
"theme.dark": "Темная",
|
||||
"theme.light": "Светлая",
|
||||
"theme.title": "Тема",
|
||||
"theme.color_primary": "Цвет темы",
|
||||
"theme.window.style.opaque": "Непрозрачное окно",
|
||||
"theme.window.style.title": "Стиль окна",
|
||||
"theme.window.style.transparent": "Прозрачное окно",
|
||||
|
||||
@ -1695,6 +1695,7 @@
|
||||
"theme.dark": "深色",
|
||||
"theme.light": "浅色",
|
||||
"theme.title": "主题",
|
||||
"theme.color_primary": "主题颜色",
|
||||
"theme.window.style.opaque": "不透明窗口",
|
||||
"theme.window.style.title": "窗口样式",
|
||||
"theme.window.style.transparent": "透明窗口",
|
||||
|
||||
@ -1688,6 +1688,7 @@
|
||||
"theme.dark": "深色",
|
||||
"theme.light": "淺色",
|
||||
"theme.title": "主題",
|
||||
"theme.color_primary": "主題顏色",
|
||||
"theme.window.style.opaque": "不透明視窗",
|
||||
"theme.window.style.title": "視窗樣式",
|
||||
"theme.window.style.transparent": "透明視窗",
|
||||
|
||||
@ -2,6 +2,7 @@ import { SyncOutlined } from '@ant-design/icons'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import useUserTheme from '@renderer/hooks/useUserTheme'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import {
|
||||
AssistantIconType,
|
||||
@ -14,9 +15,9 @@ import {
|
||||
setSidebarIcons
|
||||
} from '@renderer/store/settings'
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
import { Button, Input, Segmented, Switch } from 'antd'
|
||||
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 styled from 'styled-components'
|
||||
|
||||
@ -36,12 +37,14 @@ const DisplaySettings: FC = () => {
|
||||
pinTopicsToTop,
|
||||
customCss,
|
||||
sidebarIcons,
|
||||
assistantIconType
|
||||
assistantIconType,
|
||||
userTheme
|
||||
} = useSettings()
|
||||
const { theme: themeMode } = useTheme()
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
const [currentZoom, setCurrentZoom] = useState(1.0)
|
||||
const { setUserTheme } = useUserTheme()
|
||||
|
||||
const [visibleIcons, setVisibleIcons] = useState(sidebarIcons?.visible || DEFAULT_SIDEBAR_ICONS)
|
||||
const [disabledIcons, setDisabledIcons] = useState(sidebarIcons?.disabled || [])
|
||||
@ -53,6 +56,16 @@ const DisplaySettings: FC = () => {
|
||||
[setWindowStyle]
|
||||
)
|
||||
|
||||
const handleColorPrimaryChange = useCallback(
|
||||
(colorHex: string) => {
|
||||
setUserTheme({
|
||||
...userTheme,
|
||||
colorPrimary: colorHex
|
||||
})
|
||||
},
|
||||
[setUserTheme, userTheme]
|
||||
)
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
setVisibleIcons([...DEFAULT_SIDEBAR_ICONS])
|
||||
setDisabledIcons([])
|
||||
@ -149,6 +162,22 @@ const DisplaySettings: FC = () => {
|
||||
/>
|
||||
</ZoomButtonGroup>
|
||||
</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 && (
|
||||
<>
|
||||
<SettingDivider />
|
||||
|
||||
@ -1465,6 +1465,14 @@ const migrateConfig = {
|
||||
} catch (error) {
|
||||
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 UserTheme = {
|
||||
colorPrimary: string
|
||||
}
|
||||
|
||||
export interface SettingsState {
|
||||
showAssistants: boolean
|
||||
showTopics: boolean
|
||||
@ -49,6 +53,7 @@ export interface SettingsState {
|
||||
trayOnClose: boolean
|
||||
tray: boolean
|
||||
theme: ThemeMode
|
||||
userTheme: UserTheme
|
||||
windowStyle: 'transparent' | 'opaque'
|
||||
fontSize: number
|
||||
topicPosition: 'left' | 'right'
|
||||
@ -191,6 +196,9 @@ export const initialState: SettingsState = {
|
||||
trayOnClose: true,
|
||||
tray: true,
|
||||
theme: ThemeMode.auto,
|
||||
userTheme: {
|
||||
colorPrimary: '#00b96b'
|
||||
},
|
||||
windowStyle: 'opaque',
|
||||
fontSize: 14,
|
||||
topicPosition: 'left',
|
||||
@ -368,6 +376,9 @@ const settingsSlice = createSlice({
|
||||
setCustomCss: (state, action: PayloadAction<string>) => {
|
||||
state.customCss = action.payload
|
||||
},
|
||||
setUserTheme: (state, action: PayloadAction<UserTheme>) => {
|
||||
state.userTheme = action.payload
|
||||
},
|
||||
setFontSize: (state, action: PayloadAction<number>) => {
|
||||
state.fontSize = action.payload
|
||||
},
|
||||
@ -662,6 +673,7 @@ export const {
|
||||
setTrayOnClose,
|
||||
setTray,
|
||||
setTheme,
|
||||
setUserTheme,
|
||||
setFontSize,
|
||||
setWindowStyle,
|
||||
setTopicPosition,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user