feat(core): add edgelessTheme property and edgelessDefault theme setting (#8614)

close AF-1430 AF-1471

https://github.com/user-attachments/assets/d997ac6c-ce94-4fa4-ab34-29b36c7796ea
This commit is contained in:
JimmFly
2024-11-01 03:58:02 +00:00
parent 1f6cce2f5e
commit 10b1f233d9
28 changed files with 553 additions and 181 deletions

View File

@@ -13,7 +13,7 @@
"@affine/component": "workspace:*",
"@affine/core": "workspace:*",
"@affine/i18n": "workspace:*",
"@blocksuite/affine": "0.17.23",
"@blocksuite/affine": "0.17.25",
"@blocksuite/icons": "^2.1.67",
"@capacitor/android": "^6.1.2",
"@capacitor/core": "^6.1.2",

View File

@@ -28,7 +28,7 @@
"@affine/core": "workspace:*",
"@affine/i18n": "workspace:*",
"@affine/native": "workspace:*",
"@blocksuite/affine": "0.17.23",
"@blocksuite/affine": "0.17.25",
"@electron-forge/cli": "^7.3.0",
"@electron-forge/core": "^7.3.0",
"@electron-forge/core-utils": "^7.3.0",

View File

@@ -13,7 +13,7 @@
"@affine/component": "workspace:*",
"@affine/core": "workspace:*",
"@affine/i18n": "workspace:*",
"@blocksuite/affine": "0.17.23",
"@blocksuite/affine": "0.17.25",
"@blocksuite/icons": "^2.1.67",
"@capacitor/app": "^6.0.1",
"@capacitor/browser": "^6.0.3",

View File

@@ -13,7 +13,7 @@
"@affine/component": "workspace:*",
"@affine/core": "workspace:*",
"@affine/i18n": "workspace:*",
"@blocksuite/affine": "0.17.23",
"@blocksuite/affine": "0.17.25",
"@blocksuite/icons": "^2.1.67",
"@sentry/react": "^8.0.0",
"react": "^18.2.0",

View File

@@ -62,7 +62,7 @@
"zod": "^3.22.4"
},
"devDependencies": {
"@blocksuite/affine": "0.17.23",
"@blocksuite/affine": "0.17.25",
"@blocksuite/icons": "2.1.69",
"@chromatic-com/storybook": "^3.0.0",
"@storybook/addon-essentials": "^8.2.9",

View File

@@ -1,12 +1,26 @@
import { ThemeProvider as NextThemeProvider } from 'next-themes';
import { AppThemeService, useService } from '@toeverything/infra';
import { ThemeProvider as NextThemeProvider, useTheme } from 'next-themes';
import type { PropsWithChildren } from 'react';
import { useEffect } from 'react';
const themes = ['dark', 'light'];
export function ThemeObserver() {
const { resolvedTheme } = useTheme();
const service = useService(AppThemeService);
useEffect(() => {
service.appTheme.theme$.next(resolvedTheme);
}, [resolvedTheme, service.appTheme.theme$]);
return null;
}
export const ThemeProvider = ({ children }: PropsWithChildren) => {
return (
<NextThemeProvider themes={themes} enableSystem={true}>
{children}
<ThemeObserver />
</NextThemeProvider>
);
};

View File

@@ -16,7 +16,7 @@
"@affine/i18n": "workspace:*",
"@affine/templates": "workspace:*",
"@affine/track": "workspace:*",
"@blocksuite/affine": "0.17.23",
"@blocksuite/affine": "0.17.25",
"@blocksuite/icons": "2.1.69",
"@capacitor/app": "^6.0.1",
"@capacitor/browser": "^6.0.3",

View File

@@ -2,6 +2,7 @@ import { SettingWrapper } from '@affine/component/setting-components';
import { useI18n } from '@affine/i18n';
import { ConnectorSettings } from './connector';
import { GeneralEdgelessSetting } from './general';
import { MindMapSettings } from './mind-map';
import { NoteSettings } from './note';
import { PenSettings } from './pen';
@@ -12,6 +13,7 @@ export const Edgeless = () => {
const t = useI18n();
return (
<SettingWrapper title={t['com.affine.settings.editorSettings.edgeless']()}>
<GeneralEdgelessSetting />
<NoteSettings />
<TextSettings />
<ShapeSettings />

View File

@@ -0,0 +1,106 @@
import { Menu, MenuItem, MenuTrigger } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { EditorSettingService } from '@affine/core/modules/editor-setting';
import type { EdgelessDefaultTheme } from '@affine/core/modules/editor-setting/schema';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra';
import { useMemo } from 'react';
import { menuTrigger } from '../style.css';
const getThemeOptions = (
t: ReturnType<typeof useI18n>
): { value: EdgelessDefaultTheme; label: string }[] => [
{
value: 'specified' as EdgelessDefaultTheme,
label:
t[
'com.affine.settings.editorSettings.page.edgeless-default-theme.specified'
](),
},
{
value: 'dark' as EdgelessDefaultTheme,
label: t['com.affine.themeSettings.dark'](),
},
{
value: 'light' as EdgelessDefaultTheme,
label: t['com.affine.themeSettings.light'](),
},
{
value: 'auto' as EdgelessDefaultTheme,
label: t['com.affine.themeSettings.auto'](),
},
];
const getThemeValue = (
theme: EdgelessDefaultTheme,
t: ReturnType<typeof useI18n>
) => {
switch (theme) {
case 'dark':
return t['com.affine.themeSettings.dark']();
case 'light':
return t['com.affine.themeSettings.light']();
case 'auto':
return t['com.affine.themeSettings.auto']();
case 'specified':
return t[
'com.affine.settings.editorSettings.page.edgeless-default-theme.specified'
]();
default:
return '';
}
};
export const GeneralEdgelessSetting = () => {
const t = useI18n();
const editorSetting = useService(EditorSettingService).editorSetting;
const edgelessDefaultTheme = useLiveData(
editorSetting.settings$
).edgelessDefaultTheme;
const items = getThemeOptions(t);
const currentTheme = useMemo(() => {
return getThemeValue(edgelessDefaultTheme, t);
}, [edgelessDefaultTheme, t]);
const menuItems = useMemo(() => {
return items.map(item => {
const selected = edgelessDefaultTheme === item.value;
const onSelect = () => {
editorSetting.set('edgelessDefaultTheme', item.value);
};
return (
<MenuItem key={item.value} selected={selected} onSelect={onSelect}>
{item.label}
</MenuItem>
);
});
}, [editorSetting, items, edgelessDefaultTheme]);
return (
<SettingRow
name={t[
'com.affine.settings.editorSettings.page.edgeless-default-theme.title'
]()}
desc={t[
'com.affine.settings.editorSettings.page.edgeless-default-theme.description'
]()}
>
<Menu
items={menuItems}
contentOptions={{
align: 'end',
sideOffset: 16,
style: {
width: '280px',
},
}}
>
<MenuTrigger tooltip={currentTheme} className={menuTrigger}>
{currentTheme}
</MenuTrigger>
</Menu>
</SettingRow>
);
};

View File

@@ -7,20 +7,35 @@ import { mixpanel } from '@affine/track';
import {
ConfigExtension,
type ExtensionType,
LifeCycleWatcher,
StdIdentifier,
} from '@blocksuite/affine/block-std';
import {
ColorScheme,
EdgelessBuiltInManager,
EdgelessRootBlockSpec,
EdgelessToolExtension,
EditorSettingExtension,
FontLoaderService,
PageRootBlockSpec,
} from '@blocksuite/affine/blocks';
import {
type TelemetryEventMap,
TelemetryProvider,
type ThemeExtension,
ThemeExtensionIdentifier,
} from '@blocksuite/affine/blocks';
import { type FrameworkProvider } from '@toeverything/infra';
import {
createSignalFromObservable,
type Signal,
} from '@blocksuite/affine-shared/utils';
import type { Container } from '@blocksuite/global/di';
import {
AppThemeService,
DocService,
DocsService,
type FrameworkProvider,
} from '@toeverything/infra';
import type { Observable } from 'rxjs';
import { combineLatest, map } from 'rxjs';
import { getFontConfigExtension } from '../font-extension';
import { createDatabaseOptionsConfig } from './database-block';
@@ -42,6 +57,83 @@ function getTelemetryExtension(): ExtensionType {
};
}
function createThemeExtension(framework: FrameworkProvider) {
class AffineThemeExtension
extends LifeCycleWatcher
implements ThemeExtension
{
static override readonly key = 'affine-theme';
private readonly themes: Map<string, Signal<ColorScheme>> = new Map();
protected readonly disposables: (() => void)[] = [];
static override setup(di: Container) {
super.setup(di);
di.override(ThemeExtensionIdentifier, AffineThemeExtension, [
StdIdentifier,
]);
}
getAppTheme() {
const keyName = 'app-theme';
const cache = this.themes.get(keyName);
if (cache) return cache;
const theme$: Observable<ColorScheme> = framework
.get(AppThemeService)
.appTheme.theme$.map(theme => {
return theme === ColorScheme.Dark
? ColorScheme.Dark
: ColorScheme.Light;
});
const { signal: themeSignal, cleanup } =
createSignalFromObservable<ColorScheme>(theme$, ColorScheme.Light);
this.disposables.push(cleanup);
this.themes.set(keyName, themeSignal);
return themeSignal;
}
getEdgelessTheme(docId?: string) {
const doc =
(docId && framework.get(DocsService).list.doc$(docId).getValue()) ||
framework.get(DocService).doc;
const cache = this.themes.get(doc.id);
if (cache) return cache;
const appTheme$ = framework.get(AppThemeService).appTheme.theme$;
const docTheme$ = doc.properties$.map(props => props.edgelessColorTheme);
const theme$: Observable<ColorScheme> = combineLatest([
appTheme$,
docTheme$,
]).pipe(
map(([appTheme, docTheme]) => {
const theme = docTheme === 'system' ? appTheme : docTheme;
return theme === ColorScheme.Dark
? ColorScheme.Dark
: ColorScheme.Light;
})
);
const { signal: themeSignal, cleanup } =
createSignalFromObservable<ColorScheme>(theme$, ColorScheme.Light);
this.disposables.push(cleanup);
this.themes.set(doc.id, themeSignal);
return themeSignal;
}
override unmounted() {
this.dispose();
}
dispose() {
this.disposables.forEach(dispose => dispose());
}
}
return AffineThemeExtension;
}
function getEditorConfigExtension(
framework: FrameworkProvider
): ExtensionType[] {
@@ -63,6 +155,7 @@ export function createPageRootBlockSpec(
return [
enableAI ? AIPageRootBlockSpec : PageRootBlockSpec,
FontLoaderService,
createThemeExtension(framework),
getFontConfigExtension(),
getTelemetryExtension(),
getEditorConfigExtension(framework),
@@ -76,6 +169,7 @@ export function createEdgelessRootBlockSpec(
return [
enableAI ? AIEdgelessRootBlockSpec : EdgelessRootBlockSpec,
FontLoaderService,
createThemeExtension(framework),
EdgelessToolExtension,
EdgelessBuiltInManager,
getFontConfigExtension(),

View File

@@ -5,8 +5,11 @@ import { WorkbenchService } from '@affine/core/modules/workbench';
import { type DocMode } from '@blocksuite/affine/blocks';
import type { DocCollection } from '@blocksuite/affine/store';
import { type DocProps, DocsService, useServices } from '@toeverything/infra';
import { useTheme } from 'next-themes';
import { useCallback, useMemo } from 'react';
import { getValueByDefaultTheme } from '../../hooks/use-journal';
export const usePageHelper = (docCollection: DocCollection) => {
const {
docsService,
@@ -22,6 +25,7 @@ export const usePageHelper = (docCollection: DocCollection) => {
const workbench = workbenchService.workbench;
const docRecordList = docsService.list;
const appSidebar = appSidebarService.sidebar;
const { resolvedTheme } = useTheme();
const createPageAndOpen = useCallback(
(mode?: DocMode, open?: boolean | 'new-tab') => {
@@ -30,6 +34,15 @@ export const usePageHelper = (docCollection: DocCollection) => {
note: editorSettingService.editorSetting.get('affine:note'),
};
const page = docsService.createDoc({ docProps });
const value = getValueByDefaultTheme(
editorSettingService.editorSetting.settings$.value.edgelessDefaultTheme,
resolvedTheme || 'light'
);
docRecordList
.doc$(page.id)
.value?.setProperty('edgelessColorTheme', value);
if (mode) {
docRecordList.doc$(page.id).value?.setPrimaryMode(mode);
}
@@ -45,6 +58,7 @@ export const usePageHelper = (docCollection: DocCollection) => {
docRecordList,
docsService,
editorSettingService.editorSetting,
resolvedTheme,
workbench,
]
);

View File

@@ -3,6 +3,7 @@ import {
CheckBoxCheckLinearIcon,
CreatedEditedIcon,
DateTimeIcon,
EdgelessIcon,
FileIcon,
HistoryIcon,
NumberIcon,
@@ -15,6 +16,7 @@ import { CheckboxValue } from './checkbox';
import { CreatedByValue, UpdatedByValue } from './created-updated-by';
import { CreateDateValue, DateValue, UpdatedDateValue } from './date';
import { DocPrimaryModeValue } from './doc-primary-mode';
import { EdgelessThemeValue } from './edgeless-theme';
import { JournalValue } from './journal';
import { NumberValue } from './number';
import { TagsValue } from './tags';
@@ -90,6 +92,12 @@ export const DocPropertyTypes = {
name: 'com.affine.page-properties.property.journal',
description: 'com.affine.page-properties.property.journal.tooltips',
},
edgelessTheme: {
icon: EdgelessIcon,
value: EdgelessThemeValue,
name: 'com.affine.page-properties.property.edgelessTheme',
description: 'com.affine.page-properties.property.edgelessTheme.tooltips',
},
} as Record<
string,
{

View File

@@ -0,0 +1,6 @@
import { style } from '@vanilla-extract/css';
export const container = style({
paddingTop: '3px',
paddingBottom: '3px',
});

View File

@@ -0,0 +1,48 @@
import { PropertyValue, RadioGroup, type RadioItem } from '@affine/component';
import { useI18n } from '@affine/i18n';
import { DocService, useLiveData, useService } from '@toeverything/infra';
import { useCallback, useMemo } from 'react';
import * as styles from './edgeless-theme.css';
const getThemeOptions = (t: ReturnType<typeof useI18n>) =>
[
{
value: 'system',
label: t['com.affine.themeSettings.auto'](),
},
{
value: 'light',
label: t['com.affine.themeSettings.light'](),
},
{
value: 'dark',
label: t['com.affine.themeSettings.dark'](),
},
] satisfies RadioItem[];
export const EdgelessThemeValue = () => {
const t = useI18n();
const doc = useService(DocService).doc;
const edgelessTheme = useLiveData(doc.properties$).edgelessColorTheme;
const handleChange = useCallback(
(theme: string) => {
doc.record.setProperty('edgelessColorTheme', theme);
},
[doc]
);
const themeItems = useMemo<RadioItem[]>(() => getThemeOptions(t), [t]);
return (
<PropertyValue className={styles.container} hoverable={false}>
<RadioGroup
width={194}
itemHeight={24}
value={edgelessTheme || 'system'}
onChange={handleChange}
items={themeItems}
/>
</PropertyValue>
);
};

View File

@@ -1,4 +1,5 @@
import { EditorSettingService } from '@affine/core/modules/editor-setting';
import type { EdgelessDefaultTheme } from '@affine/core/modules/editor-setting/schema';
import { JournalService } from '@affine/core/modules/journal';
import { i18nTime } from '@affine/i18n';
import { track } from '@affine/track';
@@ -11,11 +12,30 @@ import {
useServices,
} from '@toeverything/infra';
import dayjs from 'dayjs';
import { useTheme } from 'next-themes';
import { useCallback, useMemo } from 'react';
import { WorkbenchService } from '../../modules/workbench';
import { useDocCollectionHelper } from './use-block-suite-workspace-helper';
export const getValueByDefaultTheme = (
defaultTheme: EdgelessDefaultTheme,
currentAppTheme: string
) => {
switch (defaultTheme) {
case 'dark':
return 'dark';
case 'light':
return 'light';
case 'specified':
return currentAppTheme === 'dark' ? 'dark' : 'light';
case 'auto':
return 'system';
default:
return 'system';
}
};
type MaybeDate = Date | string | number;
export const JOURNAL_DATE_FORMAT = 'YYYY-MM-DD';
@@ -35,6 +55,7 @@ function toDayjs(j?: string | false) {
*/
export const useJournalHelper = (docCollection: DocCollection) => {
const bsWorkspaceHelper = useDocCollectionHelper(docCollection);
const { resolvedTheme } = useTheme();
const { docsService, editorSettingService, journalService } = useServices({
DocsService,
EditorSettingService,
@@ -49,6 +70,13 @@ export const useJournalHelper = (docCollection: DocCollection) => {
const day = dayjs(maybeDate);
const title = day.format(JOURNAL_DATE_FORMAT);
const page = bsWorkspaceHelper.createDoc();
const value = getValueByDefaultTheme(
editorSettingService.editorSetting.settings$.value.edgelessDefaultTheme,
resolvedTheme || 'light'
);
docsService.list
.doc$(page.id)
.value?.setProperty('edgelessColorTheme', value);
docsService.list.setPrimaryMode(page.id, 'page');
// set created date to match the journal date
page.collection.setDocMeta(page.id, {
@@ -67,7 +95,13 @@ export const useJournalHelper = (docCollection: DocCollection) => {
journalService.setJournalDate(page.id, title);
return page;
},
[journalService, bsWorkspaceHelper, docsService.list, editorSettingService]
[
bsWorkspaceHelper,
editorSettingService.editorSetting,
resolvedTheme,
docsService.list,
journalService,
]
);
/**

View File

@@ -4,6 +4,7 @@ import { z } from 'zod';
export const BSEditorSettingSchema = NodePropsSchema;
export type FontFamily = 'Sans' | 'Serif' | 'Mono' | 'Custom';
export type EdgelessDefaultTheme = 'auto' | 'dark' | 'light' | 'specified';
export const fontStyleOptions = [
{ key: 'Sans', value: 'var(--affine-font-sans-family)' },
@@ -23,6 +24,9 @@ const AffineEditorSettingSchema = z.object({
fullWidthLayout: z.boolean().default(false),
displayDocInfo: z.boolean().default(true),
displayBiDirectionalLink: z.boolean().default(true),
edgelessDefaultTheme: z
.enum(['specified', 'dark', 'light', 'auto'])
.default('specified'),
});
export const EditorSettingSchema = BSEditorSettingSchema.merge(

View File

@@ -680,6 +680,7 @@
"com.affine.page-properties.property.updatedBy": "Last edited by",
"com.affine.page-properties.property.createdAt": "Created at",
"com.affine.page-properties.property.updatedAt": "Updated at",
"com.affine.page-properties.property.edgelessTheme": "Edgeless theme",
"com.affine.page-properties.property.tags.tooltips": "Add relevant identifiers or categories to the doc. Useful for organizing content, improving searchability, and grouping related docs together.",
"com.affine.page-properties.property.journal.tooltips": "Indicates that this doc is a journal entry or daily note. Facilitates easy capture of ideas, quick logging of thoughts, and ongoing personal reflection.",
"com.affine.page-properties.property.checkbox.tooltips": "Use a checkbox to indicate whether a condition is true or false. Useful for confirming options, toggling features, or tracking task states.",
@@ -694,7 +695,7 @@
"com.affine.page-properties.property.createdBy.tooltips": "Displays the author of the current doc. Useful for tracking doc ownership, accountability, and collaboration.",
"com.affine.page-properties.property.updatedBy.tooltips": "Displays the last editor of the current doc. Useful for tracking recent changes.",
"com.affine.page-properties.property.docPrimaryMode.tooltips": "Select the doc mode from Page Mode, Edgeless Mode, or Auto. Useful for choosing the best display for your content.",
"com.affine.page-properties.property.docTheme.tooltips": "Select the doc theme from Light, Dark, or System. Useful for precise control over content viewing style.",
"com.affine.page-properties.property.edgelessTheme.tooltips": "Select the doc theme from Light, Dark, or System. Useful for precise control over content viewing style.",
"com.affine.propertySidebar.property-list.section": "Properties",
"com.affine.propertySidebar.add-more.section": "Add more properties",
"com.affine.page-properties.settings.title": "customize properties",
@@ -1124,6 +1125,9 @@
"com.affine.settings.editorSettings.page.display-doc-info.title": "Display doc info",
"com.affine.settings.editorSettings.page.full-width.description": "Maximise display of content within a page.",
"com.affine.settings.editorSettings.page.full-width.title": "Full width layout",
"com.affine.settings.editorSettings.page.edgeless-default-theme.description": "Set edgeless default color scheme.",
"com.affine.settings.editorSettings.page.edgeless-default-theme.title": "Edgeless default theme",
"com.affine.settings.editorSettings.page.edgeless-default-theme.specified": "Specified by current color mode",
"com.affine.settings.editorSettings.preferences": "Preferences",
"com.affine.settings.editorSettings.preferences.export.description": "You can export the entire preferences data for backup, and the exported data can be re-imported.",
"com.affine.settings.editorSettings.preferences.export.title": "Export Settings",
@@ -1267,6 +1271,7 @@
"com.affine.themeSettings.dark": "Dark",
"com.affine.themeSettings.light": "Light",
"com.affine.themeSettings.system": "System",
"com.affine.themeSettings.auto": "Auto",
"com.affine.time.now": "now",
"com.affine.time.this-mouth": "this month",
"com.affine.time.this-week": "this week",

View File

@@ -666,6 +666,7 @@
"com.affine.page-properties.property.docPrimaryMode": "文档模式",
"com.affine.page-properties.property.text": "文本",
"com.affine.page-properties.property.updatedBy": "最后编辑者",
"com.affine.page-properties.property.edgelessTheme": "无界配色方案",
"com.affine.page-properties.property.tags.tooltips": "为文档添加相关标识或类别,有助于组织内容、提高搜索效率并将相关文档归类。",
"com.affine.page-properties.property.journal.tooltips": "标识此文档为日志条目或日记,方便捕捉灵感、快速记录或自我反省。",
"com.affine.page-properties.property.checkbox.tooltips": "用于标记该条目完成与否,适合确认选项、切换功能或跟踪任务状态。",
@@ -680,7 +681,7 @@
"com.affine.page-properties.property.createdBy.tooltips": "显示当前文档的作者,用于跟踪所有权和协作情况。",
"com.affine.page-properties.property.updatedBy.tooltips": "显示文档的最后编辑者,方便跟踪最新更改。",
"com.affine.page-properties.property.docPrimaryMode.tooltips": "选择页面模式、无边界模式或自动模式,适合根据内容选择最佳显示方式。",
"com.affine.page-properties.property.docTheme.tooltips": "选择浅色、深色或系统主题,精确控制内容的查看样式。",
"com.affine.page-properties.property.edgelessTheme.tooltips": "选择浅色、深色或系统主题,精确控制内容的查看样式。",
"com.affine.page-properties.settings.title": "自定义属性",
"com.affine.page-properties.tags.open-tags-page": "打开标签页面",
"com.affine.page-properties.tags.selector-header-title": "选择或者创建一个标签",
@@ -1088,6 +1089,9 @@
"com.affine.settings.editorSettings.page.display-doc-info.title": "显示文档信息",
"com.affine.settings.editorSettings.page.full-width.description": "文档内容的最大显示量。",
"com.affine.settings.editorSettings.page.full-width.title": "全宽布局",
"com.affine.settings.editorSettings.page.edgeless-default-theme.description": "设置默认的无界配色方案。",
"com.affine.settings.editorSettings.page.edgeless-default-theme.title": "无界默认配色方案",
"com.affine.settings.editorSettings.page.edgeless-default-theme.specified": "由当前应用的配色方案指定",
"com.affine.settings.editorSettings.preferences": "首选项",
"com.affine.settings.editorSettings.preferences.export.description": "您可以导出整个首选项数据进行备份,然后可以重新导入导出的数据。",
"com.affine.settings.editorSettings.preferences.export.title": "导出设置",
@@ -1230,6 +1234,7 @@
"com.affine.themeSettings.dark": "深色",
"com.affine.themeSettings.light": "浅色",
"com.affine.themeSettings.system": "跟随系统",
"com.affine.themeSettings.auto": "自动",
"com.affine.time.now": "刚刚",
"com.affine.time.this-mouth": "本月",
"com.affine.time.this-week": "本周",