mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-18 02:26:21 +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:
+1
-46
@@ -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']()}
|
||||
|
||||
+206
-29
@@ -1,9 +1,12 @@
|
||||
import {
|
||||
Loading,
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuSeparator,
|
||||
MenuTrigger,
|
||||
RadioGroup,
|
||||
type RadioItem,
|
||||
Scrollable,
|
||||
Switch,
|
||||
} from '@affine/component';
|
||||
import {
|
||||
@@ -11,38 +14,76 @@ import {
|
||||
SettingWrapper,
|
||||
} from '@affine/component/setting-components';
|
||||
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 {
|
||||
type AppSetting,
|
||||
type DocMode,
|
||||
type FontFamily,
|
||||
fontStyleOptions,
|
||||
useLiveData,
|
||||
useService,
|
||||
} 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 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 +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 t = useI18n();
|
||||
const [value, setValue] = useState<DocMode>('page');
|
||||
@@ -104,16 +290,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'
|
||||
|
||||
+15
@@ -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'),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import './page-detail-editor.css';
|
||||
|
||||
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 { Doc as BlockSuiteDoc, DocCollection } from '@blocksuite/store';
|
||||
import {
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
useLiveData,
|
||||
useService,
|
||||
} from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import clsx from 'clsx';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, Suspense, useCallback, useMemo } from 'react';
|
||||
@@ -56,9 +57,15 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({
|
||||
const fontStyle = fontStyleOptions.find(
|
||||
option => 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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user