feature: add option to change font (#10133)

* feature: add option to change font

1. set app global font
2. set code block font

Signed-off-by: Albert Abdilim <albert.abdilim@foxmail.com>

* formatted code with Prettier

* fix ci errors

1.add migration in `migrate.ts`
2.add to-be-translated strings by running `yarn sync:i18n`

* chore: update yarn.lock to include font-list package version 2.0.0

* fix migration issue

---------

Signed-off-by: Albert Abdilim <albert.abdilim@foxmail.com>
Co-authored-by: suyao <sy20010504@gmail.com>
This commit is contained in:
Licardo 2025-09-13 20:36:13 +08:00 committed by GitHub
parent 80afb3a86e
commit 993d497aad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 220 additions and 10 deletions

View File

@ -78,6 +78,7 @@
"@strongtz/win32-arm64-msvc": "^0.4.7", "@strongtz/win32-arm64-msvc": "^0.4.7",
"express": "^5.1.0", "express": "^5.1.0",
"faiss-node": "^0.5.1", "faiss-node": "^0.5.1",
"font-list": "^2.0.0",
"graceful-fs": "^4.2.11", "graceful-fs": "^4.2.11",
"jsdom": "26.1.0", "jsdom": "26.1.0",
"node-stream-zip": "^1.15.0", "node-stream-zip": "^1.15.0",

View File

@ -38,6 +38,7 @@ export enum IpcChannel {
App_GetDiskInfo = 'app:get-disk-info', App_GetDiskInfo = 'app:get-disk-info',
App_SetFullScreen = 'app:set-full-screen', App_SetFullScreen = 'app:set-full-screen',
App_IsFullScreen = 'app:is-full-screen', App_IsFullScreen = 'app:is-full-screen',
App_GetSystemFonts = 'app:get-system-fonts',
App_MacIsProcessTrusted = 'app:mac-is-process-trusted', App_MacIsProcessTrusted = 'app:mac-is-process-trusted',
App_MacRequestProcessTrust = 'app:mac-request-process-trust', App_MacRequestProcessTrust = 'app:mac-request-process-trust',

View File

@ -14,6 +14,7 @@ import { IpcChannel } from '@shared/IpcChannel'
import { FileMetadata, Provider, Shortcut, ThemeMode } from '@types' import { FileMetadata, Provider, Shortcut, ThemeMode } from '@types'
import checkDiskSpace from 'check-disk-space' import checkDiskSpace from 'check-disk-space'
import { BrowserWindow, dialog, ipcMain, ProxyConfig, session, shell, systemPreferences, webContents } from 'electron' import { BrowserWindow, dialog, ipcMain, ProxyConfig, session, shell, systemPreferences, webContents } from 'electron'
import fontList from 'font-list'
import { Notification } from 'src/renderer/src/types/notification' import { Notification } from 'src/renderer/src/types/notification'
import { apiServerService } from './services/ApiServerService' import { apiServerService } from './services/ApiServerService'
@ -219,6 +220,17 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
return mainWindow.isFullScreen() return mainWindow.isFullScreen()
}) })
// Get System Fonts
ipcMain.handle(IpcChannel.App_GetSystemFonts, async () => {
try {
const fonts = await fontList.getFonts()
return fonts.map((font: string) => font.replace(/^"(.*)"$/, '$1')).filter((font: string) => font.length > 0)
} catch (error) {
logger.error('Failed to get system fonts:', error as Error)
return []
}
})
ipcMain.handle(IpcChannel.Config_Set, (_, key: string, value: any, isNotify: boolean = false) => { ipcMain.handle(IpcChannel.Config_Set, (_, key: string, value: any, isNotify: boolean = false) => {
configManager.set(key, value, isNotify) configManager.set(key, value, isNotify)
}) })

View File

@ -84,6 +84,7 @@ const api = {
ipcRenderer.invoke(IpcChannel.App_LogToMain, source, level, message, data), ipcRenderer.invoke(IpcChannel.App_LogToMain, source, level, message, data),
setFullScreen: (value: boolean): Promise<void> => ipcRenderer.invoke(IpcChannel.App_SetFullScreen, value), setFullScreen: (value: boolean): Promise<void> => ipcRenderer.invoke(IpcChannel.App_SetFullScreen, value),
isFullScreen: (): Promise<boolean> => ipcRenderer.invoke(IpcChannel.App_IsFullScreen), isFullScreen: (): Promise<boolean> => ipcRenderer.invoke(IpcChannel.App_IsFullScreen),
getSystemFonts: (): Promise<string[]> => ipcRenderer.invoke(IpcChannel.App_GetSystemFonts),
mac: { mac: {
isProcessTrusted: (): Promise<boolean> => ipcRenderer.invoke(IpcChannel.App_MacIsProcessTrusted), isProcessTrusted: (): Promise<boolean> => ipcRenderer.invoke(IpcChannel.App_MacIsProcessTrusted),
requestProcessTrust: (): Promise<boolean> => ipcRenderer.invoke(IpcChannel.App_MacRequestProcessTrust) requestProcessTrust: (): Promise<boolean> => ipcRenderer.invoke(IpcChannel.App_MacRequestProcessTrust)

View File

@ -1,23 +1,24 @@
:root { :root {
--font-family: --font-family:
Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Roboto, Oxygen, Cantarell, 'Open Sans', var(--user-font-family), Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Roboto, Oxygen,
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', Cantarell, 'Open Sans', 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Noto Color Emoji'; 'Segoe UI Symbol', 'Noto Color Emoji';
--font-family-serif: --font-family-serif:
serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Ubuntu, Roboto, Oxygen, Cantarell, 'Open Sans', serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Ubuntu, Roboto, Oxygen, Cantarell, 'Open Sans',
'Helvetica Neue', Arial, 'Noto Sans', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 'Helvetica Neue', Arial, 'Noto Sans', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--code-font-family: 'Cascadia Code', 'Fira Code', 'Consolas', Menlo, Courier, monospace; --code-font-family: var(--user-code-font-family), 'Cascadia Code', 'Fira Code', 'Consolas', Menlo, Courier, monospace;
} }
/* Windows系统专用字体配置 */ /* Windows系统专用字体配置 */
body[os='windows'] { body[os='windows'] {
--font-family: --font-family:
'Twemoji Country Flags', Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Roboto, Oxygen, var(--user-font-family), 'Twemoji Country Flags', Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui,
Cantarell, 'Open Sans', 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', Roboto, Oxygen, Cantarell, 'Open Sans', 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji',
'Segoe UI Symbol', 'Noto Color Emoji'; 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--code-font-family: --code-font-family:
'Cascadia Code', 'Fira Code', 'Consolas', 'Sarasa Mono SC', 'Microsoft YaHei UI', Courier, monospace; var(--user-code-font-family), 'Cascadia Code', 'Fira Code', 'Consolas', 'Sarasa Mono SC', 'Microsoft YaHei UI',
Courier, monospace;
} }

View File

@ -15,6 +15,10 @@ export default function useUserTheme() {
document.body.style.setProperty('--primary', colorPrimary.toString()) document.body.style.setProperty('--primary', colorPrimary.toString())
document.body.style.setProperty('--color-primary-soft', colorPrimary.alpha(0.6).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()) document.body.style.setProperty('--color-primary-mute', colorPrimary.alpha(0.3).toString())
// Set font family CSS variables
document.documentElement.style.setProperty('--user-font-family', `'${theme.userFontFamily}'`)
document.documentElement.style.setProperty('--user-code-font-family', `'${theme.userCodeFontFamily}'`)
} }
return { return {

View File

@ -3116,6 +3116,13 @@
"placeholder": "/* Put custom CSS here */" "placeholder": "/* Put custom CSS here */"
} }
}, },
"font": {
"code": "Code Font",
"default": "Default",
"global": "Global Font",
"select": "Select Font",
"title": "Font Settings"
},
"navbar": { "navbar": {
"position": { "position": {
"label": "Navbar Position", "label": "Navbar Position",

View File

@ -3117,6 +3117,13 @@
"placeholder": "/* 这里写自定义 CSS */" "placeholder": "/* 这里写自定义 CSS */"
} }
}, },
"font": {
"code": "代码字体",
"default": "默认",
"global": "全局字体",
"select": "选择字体",
"title": "字体设置"
},
"navbar": { "navbar": {
"position": { "position": {
"label": "导航栏位置", "label": "导航栏位置",

View File

@ -3116,6 +3116,13 @@
"placeholder": "/* 這裡寫自訂 CSS */" "placeholder": "/* 這裡寫自訂 CSS */"
} }
}, },
"font": {
"code": "[to be translated]:代码字体",
"default": "[to be translated]:默认",
"global": "[to be translated]:全局字体",
"select": "[to be translated]:选择字体",
"title": "[to be translated]:字体设置"
},
"navbar": { "navbar": {
"position": { "position": {
"label": "導航欄位置", "label": "導航欄位置",

View File

@ -3116,6 +3116,13 @@
"placeholder": "/* Γράψτε εδώ την προσαρμοστική CSS */" "placeholder": "/* Γράψτε εδώ την προσαρμοστική CSS */"
} }
}, },
"font": {
"code": "[to be translated]:代码字体",
"default": "[to be translated]:默认",
"global": "[to be translated]:全局字体",
"select": "[to be translated]:选择字体",
"title": "[to be translated]:字体设置"
},
"navbar": { "navbar": {
"position": { "position": {
"label": "Θέση Γραμμής Πλοήγησης", "label": "Θέση Γραμμής Πλοήγησης",

View File

@ -3116,6 +3116,13 @@
"placeholder": "/* Escribe tu CSS personalizado aquí */" "placeholder": "/* Escribe tu CSS personalizado aquí */"
} }
}, },
"font": {
"code": "[to be translated]:代码字体",
"default": "[to be translated]:默认",
"global": "[to be translated]:全局字体",
"select": "[to be translated]:选择字体",
"title": "[to be translated]:字体设置"
},
"navbar": { "navbar": {
"position": { "position": {
"label": "Posición de la barra de navegación", "label": "Posición de la barra de navegación",

View File

@ -3116,6 +3116,13 @@
"placeholder": "/* Écrire votre CSS personnalisé ici */" "placeholder": "/* Écrire votre CSS personnalisé ici */"
} }
}, },
"font": {
"code": "[to be translated]:代码字体",
"default": "[to be translated]:默认",
"global": "[to be translated]:全局字体",
"select": "[to be translated]:选择字体",
"title": "[to be translated]:字体设置"
},
"navbar": { "navbar": {
"position": { "position": {
"label": "Position de la barre de navigation", "label": "Position de la barre de navigation",

View File

@ -3116,6 +3116,13 @@
"placeholder": "/* ここにカスタムCSSを入力 */" "placeholder": "/* ここにカスタムCSSを入力 */"
} }
}, },
"font": {
"code": "[to be translated]:代码字体",
"default": "[to be translated]:默认",
"global": "[to be translated]:全局字体",
"select": "[to be translated]:选择字体",
"title": "[to be translated]:字体设置"
},
"navbar": { "navbar": {
"position": { "position": {
"label": "ナビゲーションバー位置", "label": "ナビゲーションバー位置",

View File

@ -3116,6 +3116,13 @@
"placeholder": "/* Escreva seu CSS personalizado aqui */" "placeholder": "/* Escreva seu CSS personalizado aqui */"
} }
}, },
"font": {
"code": "[to be translated]:代码字体",
"default": "[to be translated]:默认",
"global": "[to be translated]:全局字体",
"select": "[to be translated]:选择字体",
"title": "[to be translated]:字体设置"
},
"navbar": { "navbar": {
"position": { "position": {
"label": "Posição da Barra de Navegação", "label": "Posição da Barra de Navegação",

View File

@ -3116,6 +3116,13 @@
"placeholder": "/* Здесь введите пользовательский CSS */" "placeholder": "/* Здесь введите пользовательский CSS */"
} }
}, },
"font": {
"code": "[to be translated]:代码字体",
"default": "[to be translated]:默认",
"global": "[to be translated]:全局字体",
"select": "[to be translated]:选择字体",
"title": "[to be translated]:字体设置"
},
"navbar": { "navbar": {
"position": { "position": {
"label": "Положение навигации", "label": "Положение навигации",

View File

@ -18,7 +18,7 @@ import {
setSidebarIcons setSidebarIcons
} from '@renderer/store/settings' } from '@renderer/store/settings'
import { ThemeMode } from '@renderer/types' import { ThemeMode } from '@renderer/types'
import { Button, ColorPicker, Segmented, Switch } from 'antd' import { Button, ColorPicker, Segmented, Select, Switch } from 'antd'
import { Minus, Monitor, Moon, Plus, Sun } from 'lucide-react' import { Minus, Monitor, Moon, Plus, Sun } from 'lucide-react'
import { FC, useCallback, useEffect, useMemo, useState } from 'react' import { FC, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -78,6 +78,7 @@ const DisplaySettings: FC = () => {
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 || [])
const [fontList, setFontList] = useState<string[]>([])
const handleWindowStyleChange = useCallback( const handleWindowStyleChange = useCallback(
(checked: boolean) => { (checked: boolean) => {
@ -136,6 +137,11 @@ const DisplaySettings: FC = () => {
) )
useEffect(() => { useEffect(() => {
// 初始化获取所有系统字体
window.api.getSystemFonts().then((fonts: string[]) => {
setFontList(fonts)
})
// 初始化获取当前缩放值 // 初始化获取当前缩放值
window.api.handleZoomFactor(0).then((factor) => { window.api.handleZoomFactor(0).then((factor) => {
setCurrentZoom(factor) setCurrentZoom(factor)
@ -160,6 +166,26 @@ const DisplaySettings: FC = () => {
setCurrentZoom(zoomFactor) setCurrentZoom(zoomFactor)
} }
const handleUserFontChange = useCallback(
(value: string) => {
setUserTheme({
...userTheme,
userFontFamily: value
})
},
[setUserTheme, userTheme]
)
const handleUserCodeFontChange = useCallback(
(value: string) => {
setUserTheme({
...userTheme,
userCodeFontFamily: value
})
},
[setUserTheme, userTheme]
)
const assistantIconTypeOptions = useMemo( const assistantIconTypeOptions = useMemo(
() => [ () => [
{ value: 'model', label: t('settings.assistant.icon.type.model') }, { value: 'model', label: t('settings.assistant.icon.type.model') },
@ -194,6 +220,7 @@ const DisplaySettings: FC = () => {
))} ))}
</HStack> </HStack>
<ColorPicker <ColorPicker
style={{ fontFamily: 'inherit' }}
className="color-picker" className="color-picker"
value={userTheme.colorPrimary} value={userTheme.colorPrimary}
onChange={(color) => handleColorPrimaryChange(color.toHexString())} onChange={(color) => handleColorPrimaryChange(color.toHexString())}
@ -255,6 +282,75 @@ const DisplaySettings: FC = () => {
</ZoomButtonGroup> </ZoomButtonGroup>
</SettingRow> </SettingRow>
</SettingGroup> </SettingGroup>
<SettingGroup theme={theme}>
<SettingTitle style={{ justifyContent: 'flex-start', gap: 5 }}>
{t('settings.display.font.title')} <TextBadge text="New" />
</SettingTitle>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.display.font.global')}</SettingRowTitle>
<SelectRow>
<Select
style={{ width: 200 }}
placeholder={t('settings.display.font.select')}
options={[
{
label: (
<span style={{ fontFamily: 'Ubuntu, -apple-system, system-ui, Arial, sans-serif' }}>
{t('settings.display.font.default')}
</span>
),
value: ''
},
...fontList.map((font) => ({ label: <span style={{ fontFamily: font }}>{font}</span>, value: font }))
]}
value={userTheme.userFontFamily || ''}
onChange={(font) => handleUserFontChange(font)}
showSearch
getPopupContainer={(triggerNode) => triggerNode.parentElement || document.body}
/>
<Button
onClick={() => handleUserFontChange('')}
style={{ marginLeft: 8 }}
icon={<ResetIcon size="14" />}
color="default"
variant="text"
/>
</SelectRow>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.display.font.code')}</SettingRowTitle>
<SelectRow>
<Select
style={{ width: 200 }}
placeholder={t('settings.display.font.select')}
options={[
{
label: (
<span style={{ fontFamily: 'Ubuntu, -apple-system, system-ui, Arial, sans-serif' }}>
{t('settings.display.font.default')}
</span>
),
value: ''
},
...fontList.map((font) => ({ label: <span style={{ fontFamily: font }}>{font}</span>, value: font }))
]}
value={userTheme.userCodeFontFamily || ''}
onChange={(font) => handleUserCodeFontChange(font)}
showSearch
getPopupContainer={(triggerNode) => triggerNode.parentElement || document.body}
/>
<Button
onClick={() => handleUserCodeFontChange('')}
style={{ marginLeft: 8 }}
icon={<ResetIcon size="14" />}
color="default"
variant="text"
/>
</SelectRow>
</SettingRow>
</SettingGroup>
<SettingGroup theme={theme}> <SettingGroup theme={theme}>
<SettingTitle>{t('settings.display.topic.title')}</SettingTitle> <SettingTitle>{t('settings.display.topic.title')}</SettingTitle>
<SettingDivider /> <SettingDivider />
@ -379,4 +475,11 @@ const ZoomValue = styled.span`
margin: 0 5px; margin: 0 5px;
` `
const SelectRow = styled.div`
display: flex;
align-items: center;
justify-content: flex-end;
width: 300px;
`
export default DisplaySettings export default DisplaySettings

View File

@ -2451,6 +2451,18 @@ const migrateConfig = {
logger.error('migrate 153 error', error as Error) logger.error('migrate 153 error', error as Error)
return state return state
} }
},
'154': (state: RootState) => {
try {
if (state.settings.userTheme) {
state.settings.userTheme.userFontFamily = settingsInitialState.userTheme.userFontFamily
state.settings.userTheme.userCodeFontFamily = settingsInitialState.userTheme.userCodeFontFamily
}
return state
} catch (error) {
logger.error('migrate 154 error', error as Error)
return state
}
} }
} }

View File

@ -33,6 +33,8 @@ export type AssistantIconType = 'model' | 'emoji' | 'none'
export type UserTheme = { export type UserTheme = {
colorPrimary: string colorPrimary: string
userFontFamily: string
userCodeFontFamily: string
} }
export interface SettingsState { export interface SettingsState {
@ -242,7 +244,9 @@ export const initialState: SettingsState = {
tray: true, tray: true,
theme: ThemeMode.system, theme: ThemeMode.system,
userTheme: { userTheme: {
colorPrimary: '#00b96b' colorPrimary: '#00b96b',
userFontFamily: '',
userCodeFontFamily: ''
}, },
windowStyle: isMac ? 'transparent' : 'opaque', windowStyle: isMac ? 'transparent' : 'opaque',
fontSize: 14, fontSize: 14,

View File

@ -13198,6 +13198,7 @@ __metadata:
fast-diff: "npm:^1.3.0" fast-diff: "npm:^1.3.0"
fast-xml-parser: "npm:^5.2.0" fast-xml-parser: "npm:^5.2.0"
fetch-socks: "npm:1.3.2" fetch-socks: "npm:1.3.2"
font-list: "npm:^2.0.0"
framer-motion: "npm:^12.23.12" framer-motion: "npm:^12.23.12"
franc-min: "npm:^6.2.0" franc-min: "npm:^6.2.0"
fs-extra: "npm:^11.2.0" fs-extra: "npm:^11.2.0"
@ -18043,6 +18044,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"font-list@npm:^2.0.0":
version: 2.0.0
resolution: "font-list@npm:2.0.0"
checksum: 10c0/9fc8600fa40a5d079982505ea101e49b21260a36f33167ac993fd7b26cec8372a16017c00d6fb404e259600ce8d588830167c9141c2df7dedb0fedd5953905f6
languageName: node
linkType: hard
"foreground-child@npm:^3.1.0": "foreground-child@npm:^3.1.0":
version: 3.3.1 version: 3.3.1
resolution: "foreground-child@npm:3.3.1" resolution: "foreground-child@npm:3.3.1"