From 993d497aad685a1189fc99b2ba20b2d8b2bcb909 Mon Sep 17 00:00:00 2001 From: Licardo <1014660822@qq.com> Date: Sat, 13 Sep 2025 20:36:13 +0800 Subject: [PATCH] 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 * 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 Co-authored-by: suyao --- package.json | 1 + packages/shared/IpcChannel.ts | 1 + src/main/ipc.ts | 12 ++ src/preload/index.ts | 1 + src/renderer/src/assets/styles/font.css | 17 +-- src/renderer/src/hooks/useUserTheme.ts | 4 + src/renderer/src/i18n/locales/en-us.json | 7 ++ src/renderer/src/i18n/locales/zh-cn.json | 7 ++ src/renderer/src/i18n/locales/zh-tw.json | 7 ++ src/renderer/src/i18n/translate/el-gr.json | 7 ++ src/renderer/src/i18n/translate/es-es.json | 7 ++ src/renderer/src/i18n/translate/fr-fr.json | 7 ++ src/renderer/src/i18n/translate/ja-jp.json | 7 ++ src/renderer/src/i18n/translate/pt-pt.json | 7 ++ src/renderer/src/i18n/translate/ru-ru.json | 7 ++ .../DisplaySettings/DisplaySettings.tsx | 105 +++++++++++++++++- src/renderer/src/store/migrate.ts | 12 ++ src/renderer/src/store/settings.ts | 6 +- yarn.lock | 8 ++ 19 files changed, 220 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index b620dd51d..23c460a6f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index fa0299168..a2ef66284 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -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', diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 6b36a96a3..1926145e8 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -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) }) diff --git a/src/preload/index.ts b/src/preload/index.ts index d8c55dd25..12cab3611 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -84,6 +84,7 @@ const api = { ipcRenderer.invoke(IpcChannel.App_LogToMain, source, level, message, data), setFullScreen: (value: boolean): Promise => ipcRenderer.invoke(IpcChannel.App_SetFullScreen, value), isFullScreen: (): Promise => ipcRenderer.invoke(IpcChannel.App_IsFullScreen), + getSystemFonts: (): Promise => ipcRenderer.invoke(IpcChannel.App_GetSystemFonts), mac: { isProcessTrusted: (): Promise => ipcRenderer.invoke(IpcChannel.App_MacIsProcessTrusted), requestProcessTrust: (): Promise => ipcRenderer.invoke(IpcChannel.App_MacRequestProcessTrust) diff --git a/src/renderer/src/assets/styles/font.css b/src/renderer/src/assets/styles/font.css index be71cef96..8e122fe88 100644 --- a/src/renderer/src/assets/styles/font.css +++ b/src/renderer/src/assets/styles/font.css @@ -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; } diff --git a/src/renderer/src/hooks/useUserTheme.ts b/src/renderer/src/hooks/useUserTheme.ts index 0b1bc5fb2..03191f598 100644 --- a/src/renderer/src/hooks/useUserTheme.ts +++ b/src/renderer/src/hooks/useUserTheme.ts @@ -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 { diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 066478b2f..24f8be0eb 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -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", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 002284335..ba0f14bbd 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -3117,6 +3117,13 @@ "placeholder": "/* 这里写自定义 CSS */" } }, + "font": { + "code": "代码字体", + "default": "默认", + "global": "全局字体", + "select": "选择字体", + "title": "字体设置" + }, "navbar": { "position": { "label": "导航栏位置", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 753981be9..5a225cbd9 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -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": "導航欄位置", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 8f1c235ec..59c775f23 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -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": "Θέση Γραμμής Πλοήγησης", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 905e720b8..47f0423ba 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -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", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 5c2a0d2a5..8d52cd894 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -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", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 391cbec4e..5ab90ecb6 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -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": "ナビゲーションバー位置", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 4d1edba05..926d88ae9 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -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", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 493a56550..cb3baa9a6 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -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": "Положение навигации", diff --git a/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx b/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx index 2bedfbbd8..b1d670792 100644 --- a/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx +++ b/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx @@ -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([]) 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 = () => { ))} handleColorPrimaryChange(color.toHexString())} @@ -255,6 +282,75 @@ const DisplaySettings: FC = () => { + + + {t('settings.display.font.title')} + + + + {t('settings.display.font.global')} + + + {t('settings.display.font.default')} + + ), + value: '' + }, + ...fontList.map((font) => ({ label: {font}, value: font })) + ]} + value={userTheme.userCodeFontFamily || ''} + onChange={(font) => handleUserCodeFontChange(font)} + showSearch + getPopupContainer={(triggerNode) => triggerNode.parentElement || document.body} + /> +