diff --git a/src/lib/components/common/InterfaceSettings.svelte b/src/lib/components/common/InterfaceSettings.svelte index 51cef5ee3b..34952d9362 100644 --- a/src/lib/components/common/InterfaceSettings.svelte +++ b/src/lib/components/common/InterfaceSettings.svelte @@ -4,7 +4,7 @@ import { toast } from 'svelte-sonner'; import { updateUserInfo } from '$lib/apis/users'; import { getUserPosition } from '$lib/utils'; - import { setTextScale } from '$lib/utils/text-scale'; + import { normalizeAppFontFamily, setAppFontFamily, setTextScale } from '$lib/utils/text-scale'; import Minus from '$lib/components/icons/Minus.svelte'; import Plus from '$lib/components/icons/Plus.svelte'; @@ -115,7 +115,10 @@ let showManageImageCompressionModal = false; let textScale: number | null = null; + let fontFamily: string | null = null; + let fontFamilyInput = ''; let showTextScaleSlider = false; + let showFontFamilyInput = false; const settingRowClass = 'flex items-center justify-between gap-2.5'; const settingLabelClass = 'min-w-0 text-xs text-gray-600 dark:text-gray-400'; const settingControlClass = 'flex shrink-0 items-center justify-end gap-1.5'; @@ -234,9 +237,18 @@ }; export const save = async () => { + const normalizedFontFamily = normalizeAppFontFamily(fontFamilyInput) || null; + fontFamily = normalizedFontFamily; + fontFamilyInput = normalizedFontFamily ?? ''; + + if (!externalSettings) { + setAppFontFamily(fontFamily || defaultFontFamily()); + } + saveSettings({ models: [defaultModelId], - imageCompressionSize: imageCompressionSize + imageCompressionSize: imageCompressionSize, + fontFamily }); }; @@ -255,6 +267,51 @@ saveSettings({ textScale }); }; + const defaultFontFamily = () => normalizeAppFontFamily(defaultSettings.fontFamily) || null; + + const previewFontFamily = (value: string) => { + fontFamilyInput = value; + fontFamily = normalizeAppFontFamily(value) || null; + + if (!externalSettings) { + setAppFontFamily(fontFamily || defaultFontFamily()); + } + }; + + const setFontFamilyHandler = (value: string | null) => { + const normalized = normalizeAppFontFamily(value); + fontFamily = normalized || null; + fontFamilyInput = normalized; + + if (!externalSettings) { + setAppFontFamily(fontFamily || defaultFontFamily()); + } + + saveSettings({ fontFamily }); + }; + + const toggleFontFamilyInput = () => { + if (!showFontFamilyInput && (fontFamily === null || isDefaultSetting('fontFamily'))) { + fontFamily = fontFamily ?? defaultFontFamily(); + fontFamilyInput = fontFamily ?? ''; + showFontFamilyInput = true; + + if (!externalSettings) { + setAppFontFamily(fontFamily); + } + } else { + showFontFamilyInput = false; + fontFamily = null; + fontFamilyInput = ''; + + if (!externalSettings) { + setAppFontFamily(defaultFontFamily()); + } + + saveSettings({ fontFamily }); + } + }; + const init = () => { titleAutoGenerate = currentSettings?.title?.auto ?? true; autoTags = currentSettings?.autoTags ?? true; @@ -338,7 +395,10 @@ webSearch = currentSettings?.webSearch ?? null; textScale = currentSettings?.textScale ?? null; + fontFamily = normalizeAppFontFamily(currentSettings?.fontFamily) || null; + fontFamilyInput = fontFamily ?? ''; showTextScaleSlider = false; + showFontFamilyInput = false; defaultUploadContext = currentSettings?.defaultUploadContext ?? 'focused'; }; @@ -488,6 +548,46 @@

+
+
+ + +
+ +
+
+ + {#if showFontFamilyInput || (fontFamily !== null && !isDefaultSetting('fontFamily'))} +
+ previewFontFamily(event.currentTarget.value)} + on:change={(event) => setFontFamilyHandler(event.currentTarget.value)} + /> +
+ {/if} +

+ {$i18n.t('Use a local font family for the app interface.')} +

+
+
diff --git a/src/lib/i18n/locales/en-GB/translation.json b/src/lib/i18n/locales/en-GB/translation.json index 4fe6986126..7445bbe4ca 100644 --- a/src/lib/i18n/locales/en-GB/translation.json +++ b/src/lib/i18n/locales/en-GB/translation.json @@ -1278,6 +1278,7 @@ "Firecrawl Timeout (s)": "", "Floating Quick Actions": "", "Focus Chat Input": "", + "Font Family": "", "Folder": "", "Folder Background Image": "", "Folder created successfully": "", @@ -2903,6 +2904,7 @@ "Use": "", "Use /v1/chat/completions endpoint instead of /v1/audio/transcriptions for potentially better accuracy.": "", "Use a Gemini API key for image generation.": "", + "Use a local font family for the app interface.": "", "Use a valid event name or pattern like user.*": "", "Use a wider chat layout on large displays.": "", "Use an API key when your ComfyUI server requires one.": "", diff --git a/src/lib/i18n/locales/en-US/translation.json b/src/lib/i18n/locales/en-US/translation.json index 6134b8ac3a..92b6b17a20 100644 --- a/src/lib/i18n/locales/en-US/translation.json +++ b/src/lib/i18n/locales/en-US/translation.json @@ -1278,6 +1278,7 @@ "Firecrawl Timeout (s)": "", "Floating Quick Actions": "", "Focus Chat Input": "", + "Font Family": "", "Folder": "", "Folder Background Image": "", "Folder created successfully": "", @@ -2903,6 +2904,7 @@ "Use": "", "Use /v1/chat/completions endpoint instead of /v1/audio/transcriptions for potentially better accuracy.": "", "Use a Gemini API key for image generation.": "", + "Use a local font family for the app interface.": "", "Use a valid event name or pattern like user.*": "", "Use a wider chat layout on large displays.": "", "Use an API key when your ComfyUI server requires one.": "", diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 34f62b7d2c..597372090b 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -231,6 +231,7 @@ type Settings = { imageCompression?: boolean; imageCompressionSize?: any; textScale?: number; + fontFamily?: string | null; widescreenMode?: null; largeTextAsFile?: boolean; promptAutocomplete?: boolean; diff --git a/src/lib/utils/text-scale.ts b/src/lib/utils/text-scale.ts index a975c13bf9..5c3876a4dd 100644 --- a/src/lib/utils/text-scale.ts +++ b/src/lib/utils/text-scale.ts @@ -1,7 +1,27 @@ -export const setTextScale = (scale) => { +export const setTextScale = (scale: number) => { if (typeof document === 'undefined') { return; } document.documentElement.style.setProperty('--app-text-scale', `${scale}`); }; + +export const normalizeAppFontFamily = (fontFamily?: string | null) => + typeof fontFamily === 'string' ? fontFamily.trim().replace(/[\u0000-\u001f\u007f]/g, '') : ''; + +export const setAppFontFamily = (fontFamily?: string | null) => { + if (typeof document === 'undefined') { + return; + } + + const normalized = normalizeAppFontFamily(fontFamily); + if (!normalized) { + document.documentElement.style.removeProperty('--app-font-family'); + return; + } + + document.documentElement.style.setProperty( + '--app-font-family', + `"${normalized.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"` + ); +}; diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index b10085df80..633a9f7ec5 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -14,7 +14,7 @@ import { getBanners } from '$lib/apis/configs'; import { getTerminalServers } from '$lib/apis/terminal'; import { getUserSettings } from '$lib/apis/users'; - import { setTextScale } from '$lib/utils/text-scale'; + import { setAppFontFamily, setTextScale } from '$lib/utils/text-scale'; import { WEBUI_VERSION, WEBUI_API_BASE_URL } from '$lib/constants'; import { compareVersion } from '$lib/utils'; @@ -98,6 +98,7 @@ loadKeybindings(userSettings?.keybindings); setTextScale($settings?.textScale ?? 1); + setAppFontFamily($settings?.fontFamily ?? null); if (cb) { await cb(); diff --git a/src/tailwind.css b/src/tailwind.css index 1526d9c1e8..6c59448417 100644 --- a/src/tailwind.css +++ b/src/tailwind.css @@ -36,7 +36,14 @@ } @layer base { - html, + html { + font-family: + var(--app-font-family, -apple-system), BlinkMacSystemFont, 'Inter', 'Vazirmatn', + ui-sans-serif, system-ui, 'Segoe UI', Roboto, Ubuntu, Cantarell, 'Noto Sans', sans-serif, + 'Helvetica Neue', Arial, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + 'Noto Color Emoji'; + } + pre { font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Vazirmatn', ui-sans-serif, system-ui, 'Segoe UI',