diff --git a/packages/common/infra/src/atom/settings.ts b/packages/common/infra/src/atom/settings.ts index 2427705f2f..30566056a6 100644 --- a/packages/common/infra/src/atom/settings.ts +++ b/packages/common/infra/src/atom/settings.ts @@ -22,6 +22,7 @@ export type AppSetting = { fullWidthLayout: boolean; windowFrameStyle: 'frameless' | 'NativeTitleBar'; fontStyle: FontFamily; + customFontFamily: string; dateFormat: DateFormats; startWeekOnMonday: boolean; enableBlurBackground: boolean; @@ -45,12 +46,13 @@ export const dateFormatOptions: DateFormats[] = [ 'dd MMMM YYYY', ]; -export type FontFamily = 'Sans' | 'Serif' | 'Mono'; +export type FontFamily = 'Sans' | 'Serif' | 'Mono' | 'Custom'; export const fontStyleOptions = [ { key: 'Sans', value: 'var(--affine-font-sans-family)' }, { key: 'Serif', value: 'var(--affine-font-serif-family)' }, { key: 'Mono', value: 'var(--affine-font-mono-family)' }, + { key: 'Custom', value: 'var(--affine-font-sans-family)' }, ] satisfies { key: FontFamily; value: string; @@ -61,6 +63,7 @@ const appSettingBaseAtom = atomWithStorage('affine-settings', { fullWidthLayout: false, windowFrameStyle: 'frameless', fontStyle: 'Sans', + customFontFamily: '', dateFormat: dateFormatOptions[0], startWeekOnMonday: false, enableBlurBackground: true, diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/appearance/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/appearance/index.tsx index f609d6a8e6..b7e0c54f25 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/appearance/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/appearance/index.tsx @@ -7,7 +7,7 @@ import { } from '@affine/component/setting-components'; import { useI18n } from '@affine/i18n'; import type { AppSetting } from '@toeverything/infra'; -import { fontStyleOptions, windowFrameStyleOptions } from '@toeverything/infra'; +import { windowFrameStyleOptions } from '@toeverything/infra'; import { useTheme } from 'next-themes'; import { useCallback, useMemo } from 'react'; @@ -58,45 +58,6 @@ export const ThemeSettings = () => { ); }; -const FontFamilySettings = () => { - const t = useI18n(); - const { appSettings, updateSettings } = useAppSettingHelper(); - - const radioItems = useMemo(() => { - return fontStyleOptions.map(({ key, value }) => { - const label = - key === 'Mono' - ? t[`com.affine.appearanceSettings.fontStyle.mono`]() - : key === 'Sans' - ? t['com.affine.appearanceSettings.fontStyle.sans']() - : key === 'Serif' - ? t['com.affine.appearanceSettings.fontStyle.serif']() - : ''; - return { - value: key, - label, - testId: 'system-font-style-trigger', - style: { fontFamily: value }, - } satisfies RadioItem; - }); - }, [t]); - - return ( - { - updateSettings('fontStyle', value); - }, - [updateSettings] - )} - /> - ); -}; - export const AppearanceSettings = () => { const t = useI18n(); @@ -116,12 +77,6 @@ export const AppearanceSettings = () => { > - - - { const t = useI18n(); const { appSettings, updateSettings } = useAppSettingHelper(); + const getLabel = useCallback( + (fontKey: FontFamily) => { + switch (fontKey) { + case 'Sans': + return t['com.affine.appearanceSettings.fontStyle.sans'](); + case 'Serif': + return t['com.affine.appearanceSettings.fontStyle.serif'](); + case 'Mono': + return t[`com.affine.appearanceSettings.fontStyle.mono`](); + case 'Custom': + return t['com.affine.settings.editorSettings.edgeless.custom'](); + default: + return ''; + } + }, + [t] + ); const radioItems = useMemo(() => { - return fontStyleOptions.map(({ key, value }) => { - const label = - key === 'Mono' - ? t[`com.affine.appearanceSettings.fontStyle.mono`]() - : key === 'Sans' - ? t['com.affine.appearanceSettings.fontStyle.sans']() - : key === 'Serif' - ? t['com.affine.appearanceSettings.fontStyle.serif']() - : ''; - return { - value: key, - label, - testId: 'system-font-style-trigger', - style: { fontFamily: value }, - } satisfies RadioItem; - }); - }, [t]); + return fontStyleOptions + .map(({ key, value }) => { + if (key === 'Custom' && !environment.isDesktop) { + return null; + } + const label = getLabel(key); + let fontFamily = value; + if (key === 'Custom' && appSettings.customFontFamily) { + fontFamily = `${appSettings.customFontFamily}, ${value}`; + } + return { + value: key, + label, + testId: 'system-font-style-trigger', + style: { + fontFamily, + }, + } satisfies RadioItem; + }) + .filter(item => item !== null); + }, [appSettings.customFontFamily, getLabel]); return ( { /> ); }; + +const getFontFamily = (font: string) => `${font}, ${fontStyleOptions[0].value}`; + +const Scroller = forwardRef< + HTMLDivElement, + PropsWithChildren> +>(({ children, ...props }, ref) => { + return ( + + + {children} + + + + ); +}); + +Scroller.displayName = 'Scroller'; + +const FontMenuItems = ({ onSelect }: { onSelect: (font: string) => void }) => { + const systemFontFamily = useService(SystemFontFamilyService).systemFontFamily; + useEffect(() => { + if (systemFontFamily.fontList$.value.length === 0) { + systemFontFamily.loadFontList(); + } + systemFontFamily.clearSearch(); + }, [systemFontFamily]); + + const isLoading = useLiveData(systemFontFamily.isLoading$); + const result = useLiveData(systemFontFamily.result$); + const searchText = useLiveData(systemFontFamily.searchText$); + + const onInputChange = useCallback( + (e: ChangeEvent) => { + systemFontFamily.search(e.target.value); + }, + [systemFontFamily] + ); + const onInputKeyDown = useCallback( + (e: React.KeyboardEvent) => { + e.stopPropagation(); // avoid typeahead search built-in in the menu + }, + [] + ); + + return ( +
+ + + {isLoading ? ( + + ) : ( + + + {result.length > 0 ? ( + ( + + )} + /> + ) : ( +
No font found
+ )} +
+ +
+ )} +
+ ); +}; + +const FontMenuItem = ({ + font, + onSelect, +}: { + font: FontData; + onSelect: (font: string) => void; +}) => { + const handleFontSelect = useCallback( + () => onSelect(font.fullName), + [font, onSelect] + ); + const fontFamily = getFontFamily(font.family); + return ( + + {font.fullName} + + ); +}; + +const CustomFontFamilySettings = () => { + const t = useI18n(); + const { appSettings, updateSettings } = useAppSettingHelper(); + const fontFamily = getFontFamily(appSettings.customFontFamily); + const onCustomFontFamilyChange = useCallback( + (fontFamily: string) => { + updateSettings('customFontFamily', fontFamily); + }, + [updateSettings] + ); + if (appSettings.fontStyle !== 'Custom' || !environment.isDesktop) { + return null; + } + return ( + + } + contentOptions={{ + align: 'end', + style: { width: '250px' }, + }} + > + + {appSettings.customFontFamily || 'Select a font'} + + + + ); +}; const NewDocDefaultModeSettings = () => { const t = useI18n(); const [value, setValue] = useState('page'); @@ -104,16 +290,7 @@ export const General = () => { >
- - - + option.key === appSettings.fontStyle ); - assertExists(fontStyle); - return fontStyle.value; - }, [appSettings.fontStyle]); + if (!fontStyle) { + return cssVar('fontSansFamily'); + } + const customFontFamily = appSettings.customFontFamily; + + return customFontFamily && fontStyle.key === 'Custom' + ? `${customFontFamily}, ${fontStyle.value}` + : fontStyle.value; + }, [appSettings.customFontFamily, appSettings.fontStyle]); const blockId = useRouterHash(); diff --git a/packages/frontend/core/src/modules/index.ts b/packages/frontend/core/src/modules/index.ts index f7076a3291..3cc3710f80 100644 --- a/packages/frontend/core/src/modules/index.ts +++ b/packages/frontend/core/src/modules/index.ts @@ -16,6 +16,7 @@ import { configurePermissionsModule } from './permissions'; import { configureWorkspacePropertiesModule } from './properties'; import { configureQuickSearchModule } from './quicksearch'; import { configureShareDocsModule } from './share-doc'; +import { configureSystemFontFamilyModule } from './system-font-family'; import { configureTagModule } from './tag'; import { configureTelemetryModule } from './telemetry'; import { configureThemeEditorModule } from './theme-editor'; @@ -41,4 +42,5 @@ export function configureCommonModules(framework: Framework) { configureExplorerModule(framework); configureThemeEditorModule(framework); configureEditorModule(framework); + configureSystemFontFamilyModule(framework); } diff --git a/packages/frontend/core/src/modules/system-font-family/entities/system-font-family.ts b/packages/frontend/core/src/modules/system-font-family/entities/system-font-family.ts new file mode 100644 index 0000000000..9e1ba47f31 --- /dev/null +++ b/packages/frontend/core/src/modules/system-font-family/entities/system-font-family.ts @@ -0,0 +1,69 @@ +import { + effect, + Entity, + fromPromise, + LiveData, + mapInto, + onComplete, + onStart, +} from '@toeverything/infra'; +import { exhaustMap } from 'rxjs'; + +export type FontData = { + family: string; + fullName: string; + postscriptName: string; + style: string; +}; + +export class SystemFontFamily extends Entity { + constructor() { + super(); + } + + readonly searchText$ = new LiveData(null); + readonly isLoading$ = new LiveData(false); + readonly fontList$ = new LiveData([]); + readonly result$ = LiveData.computed(get => { + const fontList = get(this.fontList$); + const searchText = get(this.searchText$); + if (!searchText) { + return fontList; + } + + const filteredFonts = fontList.filter(font => + font.fullName.toLowerCase().includes(searchText.toLowerCase()) + ); + return filteredFonts; + }).throttleTime(500); + + loadFontList = effect( + exhaustMap(() => { + return fromPromise(async () => { + if (!(window as any).queryLocalFonts) { + return []; + } + const fonts = await (window as any).queryLocalFonts(); + + return fonts; + }).pipe( + mapInto(this.fontList$), + // TODO: catchErrorInto(this.error$), + onStart(() => { + this.isLoading$.next(true); + }), + onComplete(() => { + this.isLoading$.next(false); + }) + ); + }) + ); + + search(searchText: string) { + this.searchText$.next(searchText); + } + + clearSearch() { + this.searchText$.next(null); + } +} diff --git a/packages/frontend/core/src/modules/system-font-family/index.ts b/packages/frontend/core/src/modules/system-font-family/index.ts new file mode 100644 index 0000000000..c78073b36c --- /dev/null +++ b/packages/frontend/core/src/modules/system-font-family/index.ts @@ -0,0 +1,11 @@ +import type { Framework } from '@toeverything/infra'; + +import { SystemFontFamily } from './entities/system-font-family'; +import { SystemFontFamilyService } from './services/system-font-family'; + +export type { FontData } from './entities/system-font-family'; +export { SystemFontFamilyService } from './services/system-font-family'; + +export function configureSystemFontFamilyModule(framework: Framework) { + framework.service(SystemFontFamilyService).entity(SystemFontFamily); +} diff --git a/packages/frontend/core/src/modules/system-font-family/services/system-font-family.ts b/packages/frontend/core/src/modules/system-font-family/services/system-font-family.ts new file mode 100644 index 0000000000..71c66b890a --- /dev/null +++ b/packages/frontend/core/src/modules/system-font-family/services/system-font-family.ts @@ -0,0 +1,8 @@ +import { Service } from '@toeverything/infra'; + +import { SystemFontFamily } from '../entities/system-font-family'; + +export class SystemFontFamilyService extends Service { + public readonly systemFontFamily = + this.framework.createEntity(SystemFontFamily); +}