feat(settings): add locale-aware font preferences

Bundle optional Inter and Outfit families, preserve the platform system default, and provide script-aware fallbacks for every supported locale.

Refs #31
This commit is contained in:
NimBold
2026-07-23 17:13:00 +03:30
parent 228db1eaa2
commit 1f1ec828f9
20 changed files with 206 additions and 10 deletions
-3
View File
@@ -5,9 +5,6 @@
<link rel="icon" type="image/png" href="/src/assets/app-icon.png" /> <link rel="icon" type="image/png" href="/src/assets/app-icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Firelink Download Manager</title> <title>Firelink Download Manager</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
</head> </head>
<body> <body>
+20
View File
@@ -9,6 +9,8 @@
"version": "1.2.0", "version": "1.2.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@fontsource-variable/inter": "^5.3.0",
"@fontsource-variable/outfit": "^5.3.0",
"@formkit/auto-animate": "^0.10.0", "@formkit/auto-animate": "^0.10.0",
"@tailwindcss/vite": "^4.3.3", "@tailwindcss/vite": "^4.3.3",
"@tauri-apps/api": "^2.11.1", "@tauri-apps/api": "^2.11.1",
@@ -81,6 +83,24 @@
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/@fontsource-variable/inter": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.3.0.tgz",
"integrity": "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource-variable/outfit": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource-variable/outfit/-/outfit-5.3.0.tgz",
"integrity": "sha512-fPb5XnYK0k7SbR++HEmm4+ZqDszmLKQ+sPRUr8oQ/KNC0vy6r6hazJFe8iYozv4cqlHbvJ/JECkJWrfDodUMVQ==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@formkit/auto-animate": { "node_modules/@formkit/auto-animate": {
"version": "0.10.0", "version": "0.10.0",
"resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.10.0.tgz", "resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.10.0.tgz",
+2
View File
@@ -39,6 +39,8 @@
"test": "vitest" "test": "vitest"
}, },
"dependencies": { "dependencies": {
"@fontsource-variable/inter": "^5.3.0",
"@fontsource-variable/outfit": "^5.3.0",
"@formkit/auto-animate": "^0.10.0", "@formkit/auto-animate": "^0.10.0",
"@tailwindcss/vite": "^4.3.3", "@tailwindcss/vite": "^4.3.3",
"@tauri-apps/api": "^2.11.1", "@tauri-apps/api": "^2.11.1",
+19
View File
@@ -214,6 +214,23 @@ pub enum Theme {
Nord, Nord,
} }
#[derive(Clone, Debug, Serialize, Deserialize, TS)]
#[serde(rename_all = "lowercase")]
#[ts(export, export_to = "../../src/bindings/")]
pub enum FontFamily {
System,
Inter,
Outfit,
Serif,
Monospace,
}
impl Default for FontFamily {
fn default() -> Self {
Self::System
}
}
#[derive(Clone, Debug, Serialize, Deserialize, TS)] #[derive(Clone, Debug, Serialize, Deserialize, TS)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
#[ts(export, export_to = "../../src/bindings/")] #[ts(export, export_to = "../../src/bindings/")]
@@ -312,6 +329,8 @@ pub struct SchedulerSettings {
pub struct PersistedSettings { pub struct PersistedSettings {
pub theme: Theme, pub theme: Theme,
#[serde(default)] #[serde(default)]
pub font_family: FontFamily,
#[serde(default)]
pub window_control_style: WindowControlStyle, pub window_control_style: WindowControlStyle,
#[serde(default)] #[serde(default)]
pub calendar_preference: CalendarPreference, pub calendar_preference: CalendarPreference,
+22 -3
View File
@@ -1,6 +1,7 @@
use crate::ipc::{ use crate::ipc::{
AppFontSize, CalendarPreference, ListRowDensity, MediaCookieSource, PersistedSettings, AppFontSize, CalendarPreference, FontFamily, ListRowDensity, MediaCookieSource,
PostQueueAction, ProxyMode, SchedulerSettings, SettingsTab, Theme, WindowControlStyle, PersistedSettings, PostQueueAction, ProxyMode, SchedulerSettings, SettingsTab, Theme,
WindowControlStyle,
}; };
use serde_json::{Map, Value}; use serde_json::{Map, Value};
use std::collections::HashMap; use std::collections::HashMap;
@@ -192,6 +193,11 @@ fn sanitize_persisted_setting_values(state: &mut Value) {
"theme", "theme",
&["system", "light", "dark", "dracula", "nord"], &["system", "light", "dark", "dracula", "nord"],
); );
sanitize_allowed_string(
state,
"fontFamily",
&["system", "inter", "outfit", "serif", "monospace"],
);
sanitize_allowed_string( sanitize_allowed_string(
state, state,
"windowControlStyle", "windowControlStyle",
@@ -422,6 +428,7 @@ fn derived_location_path(base: &str, subfolder: &str) -> String {
fn default_settings() -> PersistedSettings { fn default_settings() -> PersistedSettings {
PersistedSettings { PersistedSettings {
theme: Theme::System, theme: Theme::System,
font_family: FontFamily::System,
window_control_style: WindowControlStyle::Auto, window_control_style: WindowControlStyle::Auto,
calendar_preference: CalendarPreference::Gregorian, calendar_preference: CalendarPreference::Gregorian,
language: "system".to_string(), language: "system".to_string(),
@@ -478,7 +485,7 @@ fn default_settings() -> PersistedSettings {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::ipc::WindowControlStyle; use crate::ipc::{FontFamily, WindowControlStyle};
use super::{ use super::{
decode_stored_settings, default_settings, preserve_portable_pairing_token, decode_stored_settings, default_settings, preserve_portable_pairing_token,
preserve_scheduler_runtime_keys, preserve_scheduler_runtime_keys,
@@ -685,6 +692,18 @@ mod tests {
assert_eq!(settings.sidebar_position, "auto"); assert_eq!(settings.sidebar_position, "auto");
} }
#[test]
fn invalid_font_family_uses_system_font() {
let stored = json!({
"state": {"fontFamily": "comic-sans"},
"version": 5
});
let settings = decode_stored_settings(&Value::String(stored.to_string())).unwrap();
assert!(matches!(settings.font_family, FontFamily::System));
}
#[test] #[test]
fn invalid_window_control_style_uses_automatic_layout() { fn invalid_window_control_style_uses_automatic_layout() {
let stored = json!({ let stored = json!({
+1 -1
View File
@@ -23,7 +23,7 @@
} }
], ],
"security": { "security": {
"csp": "default-src 'self'; img-src 'self' data: https:; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws://localhost:* http://localhost:* http://127.0.0.1:* ws://127.0.0.1:*" "csp": "default-src 'self'; img-src 'self' data: https:; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'; connect-src 'self' ws://localhost:* http://localhost:* http://127.0.0.1:* ws://127.0.0.1:*"
} }
}, },
"bundle": { "bundle": {
+5
View File
@@ -194,6 +194,7 @@ function App() {
const sidebarPosition = useSettingsStore(state => state.sidebarPosition); const sidebarPosition = useSettingsStore(state => state.sidebarPosition);
const toggleSidebar = useSettingsStore(state => state.toggleSidebar); const toggleSidebar = useSettingsStore(state => state.toggleSidebar);
const activeView = useSettingsStore(state => state.activeView); const activeView = useSettingsStore(state => state.activeView);
const fontFamily = useSettingsStore(state => state.fontFamily);
const appFontSize = useSettingsStore(state => state.appFontSize); const appFontSize = useSettingsStore(state => state.appFontSize);
const listRowDensity = useSettingsStore(state => state.listRowDensity); const listRowDensity = useSettingsStore(state => state.listRowDensity);
@@ -649,6 +650,10 @@ function App() {
}); });
}, [addToast, coreReady, showKeychainModal]); }, [addToast, coreReady, showKeychainModal]);
useEffect(() => {
window.document.documentElement.setAttribute('data-font-family', fontFamily);
}, [fontFamily]);
useEffect(() => { useEffect(() => {
window.document.documentElement.setAttribute('data-font-size', appFontSize); window.document.documentElement.setAttribute('data-font-size', appFontSize);
}, [appFontSize]); }, [appFontSize]);
+3
View File
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type FontFamily = "system" | "inter" | "outfit" | "serif" | "monospace";
+2 -1
View File
@@ -1,6 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AppFontSize } from "./AppFontSize"; import type { AppFontSize } from "./AppFontSize";
import type { CalendarPreference } from "./CalendarPreference"; import type { CalendarPreference } from "./CalendarPreference";
import type { FontFamily } from "./FontFamily";
import type { ListRowDensity } from "./ListRowDensity"; import type { ListRowDensity } from "./ListRowDensity";
import type { MediaCookieSource } from "./MediaCookieSource"; import type { MediaCookieSource } from "./MediaCookieSource";
import type { ProxyMode } from "./ProxyMode"; import type { ProxyMode } from "./ProxyMode";
@@ -10,4 +11,4 @@ import type { SiteLogin } from "./SiteLogin";
import type { Theme } from "./Theme"; import type { Theme } from "./Theme";
import type { WindowControlStyle } from "./WindowControlStyle"; import type { WindowControlStyle } from "./WindowControlStyle";
export type PersistedSettings = { theme: Theme, windowControlStyle: WindowControlStyle, calendarPreference: CalendarPreference, language: string, baseDownloadFolder: string, categorySubfoldersEnabled: boolean, categorySubfolders: { [key in string]: string }, categoryDirectoryOverrides: { [key in string]: string }, approvedDownloadRoots: Array<string>, maxConcurrentDownloads: number, globalSpeedLimit: string, speedLimitPresetValues: Array<number>, logsEnabled: boolean, isSidebarVisible: boolean, sidebarPosition: string, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array<string>, schedulerLastStartKey: string, schedulerLastStopKey: string, lastCustomSpeedLimitKiB: number, lastCustomSpeedLimitUnit: string, perServerConnections: number, maxAutomaticRetries: number, showNotifications: boolean, playCompletionSound: boolean, autoAddClipboardLinks: boolean, appFontSize: AppFontSize, listRowDensity: ListRowDensity, showDockBadge: boolean, showMenuBarIcon: boolean, proxyMode: ProxyMode, proxyHost: string, proxyPort: number, customUserAgent: string, askWhereToSaveEachFile: boolean, rememberLastUsedDownloadDirectory: boolean, preventsSleepWhileDownloading: boolean, mediaCookieSource: MediaCookieSource, siteLogins: Array<SiteLogin>, autoCheckUpdates: boolean, keychainAccessGranted: boolean, }; export type PersistedSettings = { theme: Theme, fontFamily: FontFamily, windowControlStyle: WindowControlStyle, calendarPreference: CalendarPreference, language: string, baseDownloadFolder: string, categorySubfoldersEnabled: boolean, categorySubfolders: { [key in string]: string }, categoryDirectoryOverrides: { [key in string]: string }, approvedDownloadRoots: Array<string>, maxConcurrentDownloads: number, globalSpeedLimit: string, speedLimitPresetValues: Array<number>, logsEnabled: boolean, isSidebarVisible: boolean, sidebarPosition: string, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array<string>, schedulerLastStartKey: string, schedulerLastStopKey: string, lastCustomSpeedLimitKiB: number, lastCustomSpeedLimitUnit: string, perServerConnections: number, maxAutomaticRetries: number, showNotifications: boolean, playCompletionSound: boolean, autoAddClipboardLinks: boolean, appFontSize: AppFontSize, listRowDensity: ListRowDensity, showDockBadge: boolean, showMenuBarIcon: boolean, proxyMode: ProxyMode, proxyHost: string, proxyPort: number, customUserAgent: string, askWhereToSaveEachFile: boolean, rememberLastUsedDownloadDirectory: boolean, preventsSleepWhileDownloading: boolean, mediaCookieSource: MediaCookieSource, siteLogins: Array<SiteLogin>, autoCheckUpdates: boolean, keychainAccessGranted: boolean, };
+23
View File
@@ -1,6 +1,7 @@
import { useCallback, useRef, useState, useEffect } from 'react'; import { useCallback, useRef, useState, useEffect } from 'react';
import { import {
type AppFontSize, type AppFontSize,
type FontFamily,
type ListRowDensity, type ListRowDensity,
type SidebarPosition, type SidebarPosition,
type WindowControlStyle, type WindowControlStyle,
@@ -706,6 +707,13 @@ runEngineChecks(false);
{ value: 'gnome', label: t($ => $.settings.lookAndFeel.windowControlStyleGnome) }, { value: 'gnome', label: t($ => $.settings.lookAndFeel.windowControlStyleGnome) },
{ value: 'minimal', label: t($ => $.settings.lookAndFeel.windowControlStyleMinimal) }, { value: 'minimal', label: t($ => $.settings.lookAndFeel.windowControlStyleMinimal) },
]; ];
const fontFamilyOptions: Array<{ value: FontFamily; label: string }> = [
{ value: 'system', label: t($ => $.settings.lookAndFeel.fontFamilySystem) },
{ value: 'inter', label: t($ => $.settings.lookAndFeel.fontFamilyInter) },
{ value: 'outfit', label: t($ => $.settings.lookAndFeel.fontFamilyOutfit) },
{ value: 'serif', label: t($ => $.settings.lookAndFeel.fontFamilySerif) },
{ value: 'monospace', label: t($ => $.settings.lookAndFeel.fontFamilyMonospace) },
];
const TabButton = ({ type, icon: Icon, label }: { type: SettingsTab; icon: typeof Download; label: string }) => { const TabButton = ({ type, icon: Icon, label }: { type: SettingsTab; icon: typeof Download; label: string }) => {
const active = activeTab === type; const active = activeTab === type;
@@ -903,6 +911,21 @@ runEngineChecks(false);
<h2 className="settings-section-title">{t($ => $.settings.lookAndFeel.display)}</h2> <h2 className="settings-section-title">{t($ => $.settings.lookAndFeel.display)}</h2>
<div className="mac-settings-group"> <div className="mac-settings-group">
<div className="mac-settings-row">
<div className="settings-row-label">
<span>{t($ => $.settings.lookAndFeel.fontFamily)}</span>
<small>{t($ => $.settings.lookAndFeel.fontFamilyDescription)}</small>
</div>
<select
value={settings.fontFamily}
onChange={(event) => settings.setFontFamily(event.target.value as FontFamily)}
className="app-control w-60"
>
{fontFamilyOptions.map(option => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
</div>
<div className="mac-settings-row"> <div className="mac-settings-row">
<div className="settings-row-label"> <div className="settings-row-label">
<span>{t($ => $.settings.lookAndFeel.fontSize)}</span> <span>{t($ => $.settings.lookAndFeel.fontSize)}</span>
+7
View File
@@ -618,6 +618,13 @@ const common = {
nord: 'Nord', nord: 'Nord',
systemAppearance: 'System follows the current {{platform}} light or dark appearance.', systemAppearance: 'System follows the current {{platform}} light or dark appearance.',
display: 'Display', display: 'Display',
fontFamily: 'Font',
fontFamilyDescription: 'Choose the interface font. Automatic uses platform and locale-aware system fallbacks.',
fontFamilySystem: 'System UI (recommended)',
fontFamilyInter: 'Inter',
fontFamilyOutfit: 'Outfit',
fontFamilySerif: 'Serif',
fontFamilyMonospace: 'Monospace',
fontSize: 'Font size', fontSize: 'Font size',
fontSizeDescription: 'Scales the download list and compact controls.', fontSizeDescription: 'Scales the download list and compact controls.',
small: 'Small', small: 'Small',
+7
View File
@@ -618,6 +618,13 @@ const fa = {
nord: 'Nord', nord: 'Nord',
systemAppearance: 'برنامه از حالت روشن یا تاریک فعلی {{platform}} پیروی می‌کند.', systemAppearance: 'برنامه از حالت روشن یا تاریک فعلی {{platform}} پیروی می‌کند.',
display: 'نمایش', display: 'نمایش',
fontFamily: 'فونت',
fontFamilyDescription: 'فونت رابط کاربری را انتخاب کنید. حالت خودکار از فونت‌های سیستمی متناسب با سیستم‌عامل و زبان استفاده می‌کند.',
fontFamilySystem: 'رابط کاربری سیستم (پیشنهادشده)',
fontFamilyInter: 'Inter',
fontFamilyOutfit: 'Outfit',
fontFamilySerif: 'سریف',
fontFamilyMonospace: 'تک‌فاصله',
fontSize: 'اندازه قلم', fontSize: 'اندازه قلم',
fontSizeDescription: 'اندازه فهرست دانلود و کنترل‌های فشرده را تغییر می‌دهد.', fontSizeDescription: 'اندازه فهرست دانلود و کنترل‌های فشرده را تغییر می‌دهد.',
small: 'کوچک', small: 'کوچک',
+7
View File
@@ -618,6 +618,13 @@ const he = {
nord: 'Nord', nord: 'Nord',
systemAppearance: 'מערכת עוקבת אחר המראה הנוכחי (בהיר או כהה) של {{platform}}.', systemAppearance: 'מערכת עוקבת אחר המראה הנוכחי (בהיר או כהה) של {{platform}}.',
display: 'תצוגה', display: 'תצוגה',
fontFamily: 'גופן',
fontFamilyDescription: 'בחר את גופן הממשק. מצב אוטומטי משתמש בגופני מערכת המתאימים לפלטפורמה ולשפה.',
fontFamilySystem: 'ממשק המערכת (מומלץ)',
fontFamilyInter: 'Inter',
fontFamilyOutfit: 'Outfit',
fontFamilySerif: 'סריף',
fontFamilyMonospace: 'רוחב קבוע',
fontSize: 'גודל גופן', fontSize: 'גודל גופן',
fontSizeDescription: 'משנה את קנה המידה של רשימת ההורדות והפקדים הקומפקטיים.', fontSizeDescription: 'משנה את קנה המידה של רשימת ההורדות והפקדים הקומפקטיים.',
small: 'קטן', small: 'קטן',
+7
View File
@@ -618,6 +618,13 @@ const ru = {
nord: 'Nord', nord: 'Nord',
systemAppearance: 'Системная тема соответствует текущему светлому или тёмному оформлению {{platform}}.', systemAppearance: 'Системная тема соответствует текущему светлому или тёмному оформлению {{platform}}.',
display: 'Отображение', display: 'Отображение',
fontFamily: 'Шрифт',
fontFamilyDescription: 'Выберите шрифт интерфейса. Автоматический режим использует системные шрифты с учётом ОС и языка.',
fontFamilySystem: 'Системный интерфейс (рекомендуется)',
fontFamilyInter: 'Inter',
fontFamilyOutfit: 'Outfit',
fontFamilySerif: 'С засечками',
fontFamilyMonospace: 'Моноширинный',
fontSize: 'Размер шрифта', fontSize: 'Размер шрифта',
fontSizeDescription: 'Масштабирует список загрузок и компактные элементы управления.', fontSizeDescription: 'Масштабирует список загрузок и компактные элементы управления.',
small: 'Мелкий', small: 'Мелкий',
+7
View File
@@ -618,6 +618,13 @@ const uk = {
nord: 'Nord', nord: 'Nord',
systemAppearance: 'Система слідкує за поточним світлим або темним виглядом {{platform}}.', systemAppearance: 'Система слідкує за поточним світлим або темним виглядом {{platform}}.',
display: 'Відображення', display: 'Відображення',
fontFamily: 'Шрифт',
fontFamilyDescription: 'Виберіть шрифт інтерфейсу. Автоматичний режим використовує системні шрифти з урахуванням ОС і мови.',
fontFamilySystem: 'Системний інтерфейс (рекомендовано)',
fontFamilyInter: 'Inter',
fontFamilyOutfit: 'Outfit',
fontFamilySerif: 'Із зарубками',
fontFamilyMonospace: 'Моноширинний',
fontSize: 'Розмір шрифту', fontSize: 'Розмір шрифту',
fontSizeDescription: 'Масштабує список завантажень та компактні елементи керування.', fontSizeDescription: 'Масштабує список завантажень та компактні елементи керування.',
small: 'Дрібний', small: 'Дрібний',
+7
View File
@@ -618,6 +618,13 @@ const zhCN = {
nord: 'Nord', nord: 'Nord',
systemAppearance: '系统跟随当前的 {{platform}} 浅色或深色外观。', systemAppearance: '系统跟随当前的 {{platform}} 浅色或深色外观。',
display: '显示', display: '显示',
fontFamily: '字体',
fontFamilyDescription: '选择界面字体。“自动”会使用适合操作系统和语言的系统字体及备用字体。',
fontFamilySystem: '系统界面字体(推荐)',
fontFamilyInter: 'Inter',
fontFamilyOutfit: 'Outfit',
fontFamilySerif: '衬线',
fontFamilyMonospace: '等宽',
fontSize: '字体大小', fontSize: '字体大小',
fontSizeDescription: '缩放下载列表和紧凑型控件。', fontSizeDescription: '缩放下载列表和紧凑型控件。',
small: '小', small: '小',
+2
View File
@@ -94,6 +94,8 @@ describe('translation catalogs', () => {
'settings.integrations.chromiumZip', 'settings.integrations.chromiumZip',
'settings.lookAndFeel.dracula', 'settings.lookAndFeel.dracula',
'settings.lookAndFeel.nord', 'settings.lookAndFeel.nord',
'settings.lookAndFeel.fontFamilyInter',
'settings.lookAndFeel.fontFamilyOutfit',
'settings.lookAndFeel.windowControlStyleWindows', 'settings.lookAndFeel.windowControlStyleWindows',
'settings.lookAndFeel.windowControlStyleGnome', 'settings.lookAndFeel.windowControlStyleGnome',
'settings.network.chromeWindows', 'settings.network.chromeWindows',
+49 -2
View File
@@ -223,8 +223,55 @@
--color-surface-overlay: hsl(var(--surface-overlay)); --color-surface-overlay: hsl(var(--surface-overlay));
--color-bg-context-menu: hsl(var(--surface-overlay)); --color-bg-context-menu: hsl(var(--surface-overlay));
/* Apple System Fonts */ /*
--font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; * Keep the default platform UI stack, and let each locale contribute a
* script-aware fallback. Optional custom families are bundled locally so
* the Tauri app remains usable offline.
*/
--font-locale-fallback: sans-serif;
--font-stack-system: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, var(--font-locale-fallback), "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
--font-stack-inter: "Inter Variable", -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, var(--font-locale-fallback), "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
--font-stack-outfit: "Outfit Variable", -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, var(--font-locale-fallback), "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
--font-stack-serif: Georgia, "Times New Roman", var(--font-locale-fallback), serif;
--font-stack-monospace: ui-monospace, "SF Mono", Monaco, "Cascadia Code", "Fira Code", "JetBrains Mono", var(--font-locale-fallback), monospace;
--font-sans: var(--font-stack-system);
}
html[lang="fa"] {
--font-locale-fallback: "Vazirmatn", "Noto Sans Arabic", "Segoe UI", Tahoma, Arial, sans-serif;
}
html[lang="he"] {
--font-locale-fallback: "Arial Hebrew", "Noto Sans Hebrew", "Segoe UI", Arial, sans-serif;
}
html[lang="zh-CN"] {
--font-locale-fallback: "PingFang SC", "Microsoft YaHei UI", "Noto Sans CJK SC", "Noto Sans SC", sans-serif;
}
html[lang="ru"],
html[lang="uk"] {
--font-locale-fallback: "Segoe UI", "Noto Sans", Roboto, Arial, sans-serif;
}
html[data-font-family="system"] {
--font-sans: var(--font-stack-system);
}
html[data-font-family="inter"] {
--font-sans: var(--font-stack-inter);
}
html[data-font-family="outfit"] {
--font-sans: var(--font-stack-outfit);
}
html[data-font-family="serif"] {
--font-sans: var(--font-stack-serif);
}
html[data-font-family="monospace"] {
--font-sans: var(--font-stack-monospace);
} }
@layer base { @layer base {
+2
View File
@@ -1,5 +1,7 @@
import { StrictMode } from "react"; import { StrictMode } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import "@fontsource-variable/inter/wght.css";
import "@fontsource-variable/outfit/wght.css";
import "./index.css"; import "./index.css";
import App from "./App"; import App from "./App";
import { i18nReady } from "./i18n"; import { i18nReady } from "./i18n";
+14
View File
@@ -4,6 +4,7 @@ import { invokeCommand as invoke } from '../ipc';
import { info } from '../utils/logger'; import { info } from '../utils/logger';
import type { ActiveView } from '../bindings/ActiveView'; import type { ActiveView } from '../bindings/ActiveView';
import type { AppFontSize } from '../bindings/AppFontSize'; import type { AppFontSize } from '../bindings/AppFontSize';
import type { FontFamily } from '../bindings/FontFamily';
import type { ListRowDensity } from '../bindings/ListRowDensity'; import type { ListRowDensity } from '../bindings/ListRowDensity';
import type { MediaCookieSource } from '../bindings/MediaCookieSource'; import type { MediaCookieSource } from '../bindings/MediaCookieSource';
import type { PostQueueAction } from '../bindings/PostQueueAction'; import type { PostQueueAction } from '../bindings/PostQueueAction';
@@ -74,6 +75,7 @@ const notifySettingsPersistenceError = () => {
}; };
const THEME_VALUES = ['system', 'light', 'dark', 'dracula', 'nord'] as const; const THEME_VALUES = ['system', 'light', 'dark', 'dracula', 'nord'] as const;
const FONT_FAMILY_VALUES = ['system', 'inter', 'outfit', 'serif', 'monospace'] as const;
const WINDOW_CONTROL_STYLE_VALUES = ['auto', 'macos', 'windows', 'gnome', 'minimal'] as const; const WINDOW_CONTROL_STYLE_VALUES = ['auto', 'macos', 'windows', 'gnome', 'minimal'] as const;
const APP_FONT_SIZE_VALUES = ['small', 'standard', 'large'] as const; const APP_FONT_SIZE_VALUES = ['small', 'standard', 'large'] as const;
const LIST_ROW_DENSITY_VALUES = ['compact', 'standard', 'relaxed'] as const; const LIST_ROW_DENSITY_VALUES = ['compact', 'standard', 'relaxed'] as const;
@@ -162,6 +164,7 @@ const tauriStorage: StateStorage = {
export type { export type {
ActiveView, ActiveView,
AppFontSize, AppFontSize,
FontFamily,
CalendarPreference, CalendarPreference,
ListRowDensity, ListRowDensity,
MediaCookieSource, MediaCookieSource,
@@ -178,6 +181,7 @@ export type SidebarPosition = 'auto' | 'left' | 'right';
export interface SettingsState { export interface SettingsState {
theme: Theme; theme: Theme;
fontFamily: FontFamily;
windowControlStyle: WindowControlStyle; windowControlStyle: WindowControlStyle;
calendarPreference: CalendarPreference; calendarPreference: CalendarPreference;
language: AppLocalePreference; language: AppLocalePreference;
@@ -235,6 +239,7 @@ export interface SettingsState {
showKeychainModal: boolean; showKeychainModal: boolean;
setTheme: (theme: Theme) => void; setTheme: (theme: Theme) => void;
setFontFamily: (fontFamily: FontFamily) => void;
setWindowControlStyle: (style: WindowControlStyle) => void; setWindowControlStyle: (style: WindowControlStyle) => void;
setCalendarPreference: (calendarPreference: CalendarPreference) => void; setCalendarPreference: (calendarPreference: CalendarPreference) => void;
setLanguage: (language: AppLocalePreference) => void; setLanguage: (language: AppLocalePreference) => void;
@@ -293,6 +298,7 @@ export const useSettingsStore = create<SettingsState>()(
persist( persist(
(set, get) => ({ (set, get) => ({
theme: 'system', theme: 'system',
fontFamily: 'system',
windowControlStyle: 'auto', windowControlStyle: 'auto',
calendarPreference: DEFAULT_CALENDAR_PREFERENCE, calendarPreference: DEFAULT_CALENDAR_PREFERENCE,
language: 'system', language: 'system',
@@ -357,6 +363,10 @@ export const useSettingsStore = create<SettingsState>()(
showKeychainModal: false, showKeychainModal: false,
setTheme: (theme) => { info('Settings updated: theme'); set({ theme }); }, setTheme: (theme) => { info('Settings updated: theme'); set({ theme }); },
setFontFamily: (fontFamily) => {
info('Settings updated: fontFamily');
set({ fontFamily });
},
setWindowControlStyle: (windowControlStyle) => { setWindowControlStyle: (windowControlStyle) => {
info('Settings updated: windowControlStyle'); info('Settings updated: windowControlStyle');
set({ windowControlStyle }); set({ windowControlStyle });
@@ -571,6 +581,7 @@ export const useSettingsStore = create<SettingsState>()(
}, },
partialize: (state): PersistedSettingsSnapshot => ({ partialize: (state): PersistedSettingsSnapshot => ({
theme: state.theme, theme: state.theme,
fontFamily: state.fontFamily,
windowControlStyle: state.windowControlStyle, windowControlStyle: state.windowControlStyle,
calendarPreference: state.calendarPreference, calendarPreference: state.calendarPreference,
language: state.language, language: state.language,
@@ -633,6 +644,9 @@ export const useSettingsStore = create<SettingsState>()(
theme: isAllowedSetting(THEME_VALUES, persisted.theme) theme: isAllowedSetting(THEME_VALUES, persisted.theme)
? persisted.theme ? persisted.theme
: currentState.theme, : currentState.theme,
fontFamily: isAllowedSetting(FONT_FAMILY_VALUES, persisted.fontFamily)
? persisted.fontFamily
: currentState.fontFamily,
windowControlStyle: isAllowedSetting(WINDOW_CONTROL_STYLE_VALUES, persisted.windowControlStyle) windowControlStyle: isAllowedSetting(WINDOW_CONTROL_STYLE_VALUES, persisted.windowControlStyle)
? persisted.windowControlStyle ? persisted.windowControlStyle
: currentState.windowControlStyle, : currentState.windowControlStyle,