mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-27 02:42:25 +08:00
feat(core): add custom font family setting (#7924)
close AF-1255 https://github.com/user-attachments/assets/d44359b6-b75c-4883-a57b-1f226586feec
This commit is contained in:
@@ -22,6 +22,7 @@ export type AppSetting = {
|
|||||||
fullWidthLayout: boolean;
|
fullWidthLayout: boolean;
|
||||||
windowFrameStyle: 'frameless' | 'NativeTitleBar';
|
windowFrameStyle: 'frameless' | 'NativeTitleBar';
|
||||||
fontStyle: FontFamily;
|
fontStyle: FontFamily;
|
||||||
|
customFontFamily: string;
|
||||||
dateFormat: DateFormats;
|
dateFormat: DateFormats;
|
||||||
startWeekOnMonday: boolean;
|
startWeekOnMonday: boolean;
|
||||||
enableBlurBackground: boolean;
|
enableBlurBackground: boolean;
|
||||||
@@ -45,12 +46,13 @@ export const dateFormatOptions: DateFormats[] = [
|
|||||||
'dd MMMM YYYY',
|
'dd MMMM YYYY',
|
||||||
];
|
];
|
||||||
|
|
||||||
export type FontFamily = 'Sans' | 'Serif' | 'Mono';
|
export type FontFamily = 'Sans' | 'Serif' | 'Mono' | 'Custom';
|
||||||
|
|
||||||
export const fontStyleOptions = [
|
export const fontStyleOptions = [
|
||||||
{ key: 'Sans', value: 'var(--affine-font-sans-family)' },
|
{ key: 'Sans', value: 'var(--affine-font-sans-family)' },
|
||||||
{ key: 'Serif', value: 'var(--affine-font-serif-family)' },
|
{ key: 'Serif', value: 'var(--affine-font-serif-family)' },
|
||||||
{ key: 'Mono', value: 'var(--affine-font-mono-family)' },
|
{ key: 'Mono', value: 'var(--affine-font-mono-family)' },
|
||||||
|
{ key: 'Custom', value: 'var(--affine-font-sans-family)' },
|
||||||
] satisfies {
|
] satisfies {
|
||||||
key: FontFamily;
|
key: FontFamily;
|
||||||
value: string;
|
value: string;
|
||||||
@@ -61,6 +63,7 @@ const appSettingBaseAtom = atomWithStorage<AppSetting>('affine-settings', {
|
|||||||
fullWidthLayout: false,
|
fullWidthLayout: false,
|
||||||
windowFrameStyle: 'frameless',
|
windowFrameStyle: 'frameless',
|
||||||
fontStyle: 'Sans',
|
fontStyle: 'Sans',
|
||||||
|
customFontFamily: '',
|
||||||
dateFormat: dateFormatOptions[0],
|
dateFormat: dateFormatOptions[0],
|
||||||
startWeekOnMonday: false,
|
startWeekOnMonday: false,
|
||||||
enableBlurBackground: true,
|
enableBlurBackground: true,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from '@affine/component/setting-components';
|
} from '@affine/component/setting-components';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import type { AppSetting } from '@toeverything/infra';
|
import type { AppSetting } from '@toeverything/infra';
|
||||||
import { fontStyleOptions, windowFrameStyleOptions } from '@toeverything/infra';
|
import { windowFrameStyleOptions } from '@toeverything/infra';
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
import { useCallback, useMemo } from 'react';
|
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 (
|
|
||||||
<RadioGroup
|
|
||||||
items={radioItems}
|
|
||||||
value={appSettings.fontStyle}
|
|
||||||
width={250}
|
|
||||||
className={settingWrapper}
|
|
||||||
onChange={useCallback(
|
|
||||||
(value: AppSetting['fontStyle']) => {
|
|
||||||
updateSettings('fontStyle', value);
|
|
||||||
},
|
|
||||||
[updateSettings]
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AppearanceSettings = () => {
|
export const AppearanceSettings = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
@@ -116,12 +77,6 @@ export const AppearanceSettings = () => {
|
|||||||
>
|
>
|
||||||
<ThemeSettings />
|
<ThemeSettings />
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingRow
|
|
||||||
name={t['com.affine.appearanceSettings.font.title']()}
|
|
||||||
desc={t['com.affine.appearanceSettings.font.description']()}
|
|
||||||
>
|
|
||||||
<FontFamilySettings />
|
|
||||||
</SettingRow>
|
|
||||||
<SettingRow
|
<SettingRow
|
||||||
name={t['com.affine.appearanceSettings.language.title']()}
|
name={t['com.affine.appearanceSettings.language.title']()}
|
||||||
desc={t['com.affine.appearanceSettings.language.description']()}
|
desc={t['com.affine.appearanceSettings.language.description']()}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
|
Loading,
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
|
MenuSeparator,
|
||||||
MenuTrigger,
|
MenuTrigger,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
type RadioItem,
|
type RadioItem,
|
||||||
|
Scrollable,
|
||||||
Switch,
|
Switch,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import {
|
import {
|
||||||
@@ -11,38 +14,76 @@ import {
|
|||||||
SettingWrapper,
|
SettingWrapper,
|
||||||
} from '@affine/component/setting-components';
|
} from '@affine/component/setting-components';
|
||||||
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
|
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
|
||||||
|
import {
|
||||||
|
type FontData,
|
||||||
|
SystemFontFamilyService,
|
||||||
|
} from '@affine/core/modules/system-font-family';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import {
|
import {
|
||||||
type AppSetting,
|
type AppSetting,
|
||||||
type DocMode,
|
type DocMode,
|
||||||
|
type FontFamily,
|
||||||
fontStyleOptions,
|
fontStyleOptions,
|
||||||
|
useLiveData,
|
||||||
|
useService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import {
|
||||||
|
type ChangeEvent,
|
||||||
|
forwardRef,
|
||||||
|
type HTMLAttributes,
|
||||||
|
type PropsWithChildren,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
|
|
||||||
import { menu, menuTrigger, settingWrapper } from './style.css';
|
import { menu, menuTrigger, searchInput, settingWrapper } from './style.css';
|
||||||
|
|
||||||
const FontFamilySettings = () => {
|
const FontFamilySettings = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { appSettings, updateSettings } = useAppSettingHelper();
|
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(() => {
|
const radioItems = useMemo(() => {
|
||||||
return fontStyleOptions.map(({ key, value }) => {
|
return fontStyleOptions
|
||||||
const label =
|
.map(({ key, value }) => {
|
||||||
key === 'Mono'
|
if (key === 'Custom' && !environment.isDesktop) {
|
||||||
? t[`com.affine.appearanceSettings.fontStyle.mono`]()
|
return null;
|
||||||
: key === 'Sans'
|
}
|
||||||
? t['com.affine.appearanceSettings.fontStyle.sans']()
|
const label = getLabel(key);
|
||||||
: key === 'Serif'
|
let fontFamily = value;
|
||||||
? t['com.affine.appearanceSettings.fontStyle.serif']()
|
if (key === 'Custom' && appSettings.customFontFamily) {
|
||||||
: '';
|
fontFamily = `${appSettings.customFontFamily}, ${value}`;
|
||||||
return {
|
}
|
||||||
value: key,
|
return {
|
||||||
label,
|
value: key,
|
||||||
testId: 'system-font-style-trigger',
|
label,
|
||||||
style: { fontFamily: value },
|
testId: 'system-font-style-trigger',
|
||||||
} satisfies RadioItem;
|
style: {
|
||||||
});
|
fontFamily,
|
||||||
}, [t]);
|
},
|
||||||
|
} satisfies RadioItem;
|
||||||
|
})
|
||||||
|
.filter(item => item !== null);
|
||||||
|
}, [appSettings.customFontFamily, getLabel]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
@@ -59,6 +100,151 @@ const FontFamilySettings = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getFontFamily = (font: string) => `${font}, ${fontStyleOptions[0].value}`;
|
||||||
|
|
||||||
|
const Scroller = forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
PropsWithChildren<HTMLAttributes<HTMLDivElement>>
|
||||||
|
>(({ children, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<Scrollable.Root>
|
||||||
|
<Scrollable.Viewport {...props} ref={ref}>
|
||||||
|
{children}
|
||||||
|
</Scrollable.Viewport>
|
||||||
|
<Scrollable.Scrollbar />
|
||||||
|
</Scrollable.Root>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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<HTMLInputElement>) => {
|
||||||
|
systemFontFamily.search(e.target.value);
|
||||||
|
},
|
||||||
|
[systemFontFamily]
|
||||||
|
);
|
||||||
|
const onInputKeyDown = useCallback(
|
||||||
|
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
e.stopPropagation(); // avoid typeahead search built-in in the menu
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
value={searchText ?? ''}
|
||||||
|
onChange={onInputChange}
|
||||||
|
onKeyDown={onInputKeyDown}
|
||||||
|
autoFocus
|
||||||
|
className={searchInput}
|
||||||
|
placeholder="Type here ..."
|
||||||
|
/>
|
||||||
|
<MenuSeparator />
|
||||||
|
{isLoading ? (
|
||||||
|
<Loading />
|
||||||
|
) : (
|
||||||
|
<Scrollable.Root style={{ height: '200px' }}>
|
||||||
|
<Scrollable.Viewport>
|
||||||
|
{result.length > 0 ? (
|
||||||
|
<Virtuoso
|
||||||
|
totalCount={result.length}
|
||||||
|
components={{
|
||||||
|
Scroller: Scroller,
|
||||||
|
}}
|
||||||
|
itemContent={index => (
|
||||||
|
<FontMenuItem
|
||||||
|
key={result[index].fullName}
|
||||||
|
font={result[index]}
|
||||||
|
onSelect={onSelect}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div>No font found</div>
|
||||||
|
)}
|
||||||
|
</Scrollable.Viewport>
|
||||||
|
<Scrollable.Scrollbar />
|
||||||
|
</Scrollable.Root>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FontMenuItem = ({
|
||||||
|
font,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
font: FontData;
|
||||||
|
onSelect: (font: string) => void;
|
||||||
|
}) => {
|
||||||
|
const handleFontSelect = useCallback(
|
||||||
|
() => onSelect(font.fullName),
|
||||||
|
[font, onSelect]
|
||||||
|
);
|
||||||
|
const fontFamily = getFontFamily(font.family);
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
key={font.fullName}
|
||||||
|
onSelect={handleFontSelect}
|
||||||
|
style={{ fontFamily }}
|
||||||
|
>
|
||||||
|
{font.fullName}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<SettingRow
|
||||||
|
name={t[
|
||||||
|
'com.affine.settings.editorSettings.general.font-family.custom.title'
|
||||||
|
]()}
|
||||||
|
desc={t[
|
||||||
|
'com.affine.settings.editorSettings.general.font-family.custom.description'
|
||||||
|
]()}
|
||||||
|
>
|
||||||
|
<Menu
|
||||||
|
items={<FontMenuItems onSelect={onCustomFontFamilyChange} />}
|
||||||
|
contentOptions={{
|
||||||
|
align: 'end',
|
||||||
|
style: { width: '250px' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuTrigger className={menuTrigger} style={{ fontFamily }}>
|
||||||
|
{appSettings.customFontFamily || 'Select a font'}
|
||||||
|
</MenuTrigger>
|
||||||
|
</Menu>
|
||||||
|
</SettingRow>
|
||||||
|
);
|
||||||
|
};
|
||||||
const NewDocDefaultModeSettings = () => {
|
const NewDocDefaultModeSettings = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const [value, setValue] = useState<DocMode>('page');
|
const [value, setValue] = useState<DocMode>('page');
|
||||||
@@ -104,16 +290,7 @@ export const General = () => {
|
|||||||
>
|
>
|
||||||
<FontFamilySettings />
|
<FontFamilySettings />
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingRow
|
<CustomFontFamilySettings />
|
||||||
name={t[
|
|
||||||
'com.affine.settings.editorSettings.general.font-family.custom.title'
|
|
||||||
]()}
|
|
||||||
desc={t[
|
|
||||||
'com.affine.settings.editorSettings.general.font-family.custom.description'
|
|
||||||
]()}
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</SettingRow>
|
|
||||||
<SettingRow
|
<SettingRow
|
||||||
name={t[
|
name={t[
|
||||||
'com.affine.settings.editorSettings.general.font-family.title'
|
'com.affine.settings.editorSettings.general.font-family.title'
|
||||||
|
|||||||
@@ -49,3 +49,18 @@ export const shapeIndicator = style({
|
|||||||
boxShadow: 'none',
|
boxShadow: 'none',
|
||||||
backgroundColor: cssVarV2('layer/background/tertiary'),
|
backgroundColor: cssVarV2('layer/background/tertiary'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const searchInput = style({
|
||||||
|
flexGrow: 1,
|
||||||
|
padding: '10px 0',
|
||||||
|
margin: '-10px 0',
|
||||||
|
border: 'none',
|
||||||
|
outline: 'none',
|
||||||
|
fontSize: cssVar('fontSm'),
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
color: 'inherit',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
'::placeholder': {
|
||||||
|
color: cssVarV2('text/placeholder'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import './page-detail-editor.css';
|
import './page-detail-editor.css';
|
||||||
|
|
||||||
import { useDocCollectionPage } from '@affine/core/hooks/use-block-suite-workspace-page';
|
import { useDocCollectionPage } from '@affine/core/hooks/use-block-suite-workspace-page';
|
||||||
import { assertExists, DisposableGroup } from '@blocksuite/global/utils';
|
import { DisposableGroup } from '@blocksuite/global/utils';
|
||||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||||
import type { Doc as BlockSuiteDoc, DocCollection } from '@blocksuite/store';
|
import type { Doc as BlockSuiteDoc, DocCollection } from '@blocksuite/store';
|
||||||
import {
|
import {
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
useLiveData,
|
useLiveData,
|
||||||
useService,
|
useService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import { memo, Suspense, useCallback, useMemo } from 'react';
|
import { memo, Suspense, useCallback, useMemo } from 'react';
|
||||||
@@ -56,9 +57,15 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({
|
|||||||
const fontStyle = fontStyleOptions.find(
|
const fontStyle = fontStyleOptions.find(
|
||||||
option => option.key === appSettings.fontStyle
|
option => option.key === appSettings.fontStyle
|
||||||
);
|
);
|
||||||
assertExists(fontStyle);
|
if (!fontStyle) {
|
||||||
return fontStyle.value;
|
return cssVar('fontSansFamily');
|
||||||
}, [appSettings.fontStyle]);
|
}
|
||||||
|
const customFontFamily = appSettings.customFontFamily;
|
||||||
|
|
||||||
|
return customFontFamily && fontStyle.key === 'Custom'
|
||||||
|
? `${customFontFamily}, ${fontStyle.value}`
|
||||||
|
: fontStyle.value;
|
||||||
|
}, [appSettings.customFontFamily, appSettings.fontStyle]);
|
||||||
|
|
||||||
const blockId = useRouterHash();
|
const blockId = useRouterHash();
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { configurePermissionsModule } from './permissions';
|
|||||||
import { configureWorkspacePropertiesModule } from './properties';
|
import { configureWorkspacePropertiesModule } from './properties';
|
||||||
import { configureQuickSearchModule } from './quicksearch';
|
import { configureQuickSearchModule } from './quicksearch';
|
||||||
import { configureShareDocsModule } from './share-doc';
|
import { configureShareDocsModule } from './share-doc';
|
||||||
|
import { configureSystemFontFamilyModule } from './system-font-family';
|
||||||
import { configureTagModule } from './tag';
|
import { configureTagModule } from './tag';
|
||||||
import { configureTelemetryModule } from './telemetry';
|
import { configureTelemetryModule } from './telemetry';
|
||||||
import { configureThemeEditorModule } from './theme-editor';
|
import { configureThemeEditorModule } from './theme-editor';
|
||||||
@@ -41,4 +42,5 @@ export function configureCommonModules(framework: Framework) {
|
|||||||
configureExplorerModule(framework);
|
configureExplorerModule(framework);
|
||||||
configureThemeEditorModule(framework);
|
configureThemeEditorModule(framework);
|
||||||
configureEditorModule(framework);
|
configureEditorModule(framework);
|
||||||
|
configureSystemFontFamilyModule(framework);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<string | null>(null);
|
||||||
|
readonly isLoading$ = new LiveData<boolean>(false);
|
||||||
|
readonly fontList$ = new LiveData<FontData[]>([]);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user