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",
"express": "^5.1.0",
"faiss-node": "^0.5.1",
"font-list": "^2.0.0",
"graceful-fs": "^4.2.11",
"jsdom": "26.1.0",
"node-stream-zip": "^1.15.0",

View File

@ -38,6 +38,7 @@ export enum IpcChannel {
App_GetDiskInfo = 'app:get-disk-info',
App_SetFullScreen = 'app:set-full-screen',
App_IsFullScreen = 'app:is-full-screen',
App_GetSystemFonts = 'app:get-system-fonts',
App_MacIsProcessTrusted = 'app:mac-is-process-trusted',
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 checkDiskSpace from 'check-disk-space'
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 { apiServerService } from './services/ApiServerService'
@ -219,6 +220,17 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
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) => {
configManager.set(key, value, isNotify)
})

View File

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

View File

@ -1,23 +1,24 @@
:root {
--font-family:
Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Roboto, Oxygen, Cantarell, 'Open Sans',
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
var(--user-font-family), Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Roboto, Oxygen,
Cantarell, 'Open Sans', 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
--font-family-serif:
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';
--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系统专用字体配置 */
body[os='windows'] {
--font-family:
'Twemoji Country Flags', Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Roboto, Oxygen,
Cantarell, 'Open Sans', 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
var(--user-font-family), 'Twemoji Country Flags', Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui,
Roboto, Oxygen, Cantarell, 'Open Sans', 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--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('--color-primary-soft', colorPrimary.alpha(0.6).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 {

View File

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

View File

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

View File

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

View File

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

View File

@ -3116,6 +3116,13 @@
"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": {
"position": {
"label": "Posición de la barra de navegación",

View File

@ -3116,6 +3116,13 @@
"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": {
"position": {
"label": "Position de la barre de navigation",

View File

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

View File

@ -3116,6 +3116,13 @@
"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": {
"position": {
"label": "Posição da Barra de Navegação",

View File

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

View File

@ -18,7 +18,7 @@ import {
setSidebarIcons
} from '@renderer/store/settings'
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 { FC, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -78,6 +78,7 @@ const DisplaySettings: FC = () => {
const [visibleIcons, setVisibleIcons] = useState(sidebarIcons?.visible || DEFAULT_SIDEBAR_ICONS)
const [disabledIcons, setDisabledIcons] = useState(sidebarIcons?.disabled || [])
const [fontList, setFontList] = useState<string[]>([])
const handleWindowStyleChange = useCallback(
(checked: boolean) => {
@ -136,6 +137,11 @@ const DisplaySettings: FC = () => {
)
useEffect(() => {
// 初始化获取所有系统字体
window.api.getSystemFonts().then((fonts: string[]) => {
setFontList(fonts)
})
// 初始化获取当前缩放值
window.api.handleZoomFactor(0).then((factor) => {
setCurrentZoom(factor)
@ -160,6 +166,26 @@ const DisplaySettings: FC = () => {
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(
() => [
{ value: 'model', label: t('settings.assistant.icon.type.model') },
@ -194,6 +220,7 @@ const DisplaySettings: FC = () => {
))}
</HStack>
<ColorPicker
style={{ fontFamily: 'inherit' }}
className="color-picker"
value={userTheme.colorPrimary}
onChange={(color) => handleColorPrimaryChange(color.toHexString())}
@ -255,6 +282,75 @@ const DisplaySettings: FC = () => {
</ZoomButtonGroup>
</SettingRow>
</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}>
<SettingTitle>{t('settings.display.topic.title')}</SettingTitle>
<SettingDivider />
@ -379,4 +475,11 @@ const ZoomValue = styled.span`
margin: 0 5px;
`
const SelectRow = styled.div`
display: flex;
align-items: center;
justify-content: flex-end;
width: 300px;
`
export default DisplaySettings

View File

@ -2451,6 +2451,18 @@ const migrateConfig = {
logger.error('migrate 153 error', error as Error)
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 = {
colorPrimary: string
userFontFamily: string
userCodeFontFamily: string
}
export interface SettingsState {
@ -242,7 +244,9 @@ export const initialState: SettingsState = {
tray: true,
theme: ThemeMode.system,
userTheme: {
colorPrimary: '#00b96b'
colorPrimary: '#00b96b',
userFontFamily: '',
userCodeFontFamily: ''
},
windowStyle: isMac ? 'transparent' : 'opaque',
fontSize: 14,

View File

@ -13198,6 +13198,7 @@ __metadata:
fast-diff: "npm:^1.3.0"
fast-xml-parser: "npm:^5.2.0"
fetch-socks: "npm:1.3.2"
font-list: "npm:^2.0.0"
framer-motion: "npm:^12.23.12"
franc-min: "npm:^6.2.0"
fs-extra: "npm:^11.2.0"
@ -18043,6 +18044,13 @@ __metadata:
languageName: node
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":
version: 3.3.1
resolution: "foreground-child@npm:3.3.1"