feat(core): add custom font family setting

This commit is contained in:
Jimmfly
2024-08-19 22:28:39 +08:00
committed by EYHN
parent d28a6300d9
commit 76bc48dd9c
5 changed files with 185 additions and 78 deletions

View File

@@ -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 (
<RadioGroup
items={radioItems}
value={appSettings.fontStyle}
width={250}
className={settingWrapper}
onChange={useCallback(
(value: AppSetting['fontStyle']) => {
updateSettings('fontStyle', value);
},
[updateSettings]
)}
/>
);
};
export const AppearanceSettings = () => {
const t = useI18n();
@@ -116,12 +77,6 @@ export const AppearanceSettings = () => {
>
<ThemeSettings />
</SettingRow>
<SettingRow
name={t['com.affine.appearanceSettings.font.title']()}
desc={t['com.affine.appearanceSettings.font.description']()}
>
<FontFamilySettings />
</SettingRow>
<SettingRow
name={t['com.affine.appearanceSettings.language.title']()}
desc={t['com.affine.appearanceSettings.language.description']()}

View File

@@ -1,9 +1,12 @@
import {
Loading,
Menu,
MenuItem,
MenuSeparator,
MenuTrigger,
RadioGroup,
type RadioItem,
Scrollable,
Switch,
} from '@affine/component';
import {
@@ -11,38 +14,63 @@ import {
SettingWrapper,
} from '@affine/component/setting-components';
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
import { SystemFontFamilyService } from '@affine/core/modules/system-font-family/services/system-font-family';
import { useI18n } from '@affine/i18n';
import {
type AppSetting,
type DocMode,
type FontFamily,
fontStyleOptions,
useLiveData,
useService,
} from '@toeverything/infra';
import { useCallback, useMemo, useState } from 'react';
import { type ChangeEvent, useCallback, useMemo, useState } from 'react';
import { menu, menuTrigger, settingWrapper } from './style.css';
import { menu, menuTrigger, searchInput, settingWrapper } from './style.css';
const FontFamilySettings = () => {
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 (
<RadioGroup
@@ -59,6 +87,116 @@ const FontFamilySettings = () => {
/>
);
};
const getFontFamily = (font: string) => `${font}, ${fontStyleOptions[0].value}`;
const FontMenuItems = ({ onSelect }: { onSelect: (font: string) => void }) => {
const systemFontFamily = useService(SystemFontFamilyService).systemFontFamily;
const systemFontList = useLiveData(systemFontFamily.fontList$);
const isLoading = useLiveData(systemFontFamily.isLoading$);
const result = useLiveData(systemFontFamily.result$);
const searchText = useLiveData(systemFontFamily.searchText$);
const [inputValue, setInputValue] = useState('');
const onInputChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
}, []);
const onInputKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
systemFontFamily.search(inputValue);
} else if (e.key === 'Backspace' || e.key === 'Escape') {
systemFontFamily.clearSearch();
}
},
[inputValue, systemFontFamily]
);
return (
<div>
<input
value={inputValue}
onChange={onInputChange}
onKeyDown={onInputKeyDown}
autoFocus
className={searchInput}
placeholder="Type here ..."
/>
<MenuSeparator />
{isLoading ? (
<Loading />
) : (
<Scrollable.Root style={{ height: '200px' }}>
<Scrollable.Viewport>
{result.length > 0 ? (
result.map(font => (
<FontMenuItem key={font} font={font} onSelect={onSelect} />
))
) : searchText && searchText.length > 0 ? (
<div>not found</div>
) : (
systemFontList.map(font => (
<FontMenuItem key={font} font={font} onSelect={onSelect} />
))
)}
</Scrollable.Viewport>
<Scrollable.Scrollbar />
</Scrollable.Root>
)}
</div>
);
};
const FontMenuItem = ({
font,
onSelect,
}: {
font: string;
onSelect: (font: string) => void;
}) => {
const handleFontSelect = useCallback(() => onSelect(font), [font, onSelect]);
const fontFamily = getFontFamily(font);
return (
<MenuItem key={font} onSelect={handleFontSelect} style={{ fontFamily }}>
{font}
</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',
}}
>
<MenuTrigger className={menuTrigger} style={{ fontFamily }}>
{appSettings.customFontFamily || 'Select a font'}
</MenuTrigger>
</Menu>
</SettingRow>
);
};
const NewDocDefaultModeSettings = () => {
const t = useI18n();
const [value, setValue] = useState<DocMode>('page');
@@ -104,16 +242,7 @@ export const General = () => {
>
<FontFamilySettings />
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.general.font-family.custom.title'
]()}
desc={t[
'com.affine.settings.editorSettings.general.font-family.custom.description'
]()}
>
<Switch />
</SettingRow>
<CustomFontFamilySettings />
<SettingRow
name={t[
'com.affine.settings.editorSettings.general.font-family.title'

View File

@@ -49,3 +49,18 @@ export const shapeIndicator = style({
boxShadow: 'none',
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'),
},
});

View File

@@ -57,8 +57,13 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({
option => option.key === appSettings.fontStyle
);
assertExists(fontStyle);
return fontStyle.value;
}, [appSettings.fontStyle]);
const customFontFamily = appSettings.customFontFamily;
return customFontFamily && fontStyle.key === 'Custom'
? `${customFontFamily}, ${fontStyle.value}`
: fontStyle.value;
}, [appSettings.customFontFamily, appSettings.fontStyle]);
const blockId = useRouterHash();