mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
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:
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const container = style({
|
||||
paddingTop: '3px',
|
||||
paddingBottom: '3px',
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user