mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-18 06:47:02 +08:00
feat(core): add custom font family setting
This commit is contained in:
@@ -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']()}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user