mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-16 13:57:02 +08:00
feat(editor): implement view extension manager with builder pattern (#12193)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Refactor** - Streamlined and modularized the configuration of editor and theme extensions, introducing a chainable API for extension management. - Migrated extension setup to use new provider classes and centralized patch logic, improving maintainability and consistency. - Updated internal extension retrieval processes to use a more explicit, stepwise initialization sequence. - Removed legacy theme and editor config registration from common views and editor setups. - Removed direct patch registrations from editor views, consolidating them into extension providers. - Renamed classes and variables for clarity and consistency across the codebase. - **New Features** - Added new extension providers for editor configuration, theme management, and edgeless block header customization, enabling more flexible and validated extension registration. - Introduced animated viewport focus and dynamic header rendering for edgeless notes and embedded synced documents. - Integrated reactive editor settings and toolbar configurations with workspace-aware base URL resolution. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -43,17 +43,20 @@ import type {
|
||||
AffineAIPanelWidgetConfig,
|
||||
} from '../widgets/ai-panel/type';
|
||||
|
||||
export const getCustomPageEditorBlockSpecs: () => ExtensionType[] = () => [
|
||||
...getViewManager().get('page'),
|
||||
{
|
||||
setup: di => {
|
||||
di.override(
|
||||
BlockViewIdentifier('affine:page'),
|
||||
() => literal`affine-page-root`
|
||||
);
|
||||
export const getCustomPageEditorBlockSpecs: () => ExtensionType[] = () => {
|
||||
const manager = getViewManager().config.init().value;
|
||||
return [
|
||||
...manager.get('page'),
|
||||
{
|
||||
setup: di => {
|
||||
di.override(
|
||||
BlockViewIdentifier('affine:page'),
|
||||
() => literal`affine-page-root`
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
];
|
||||
};
|
||||
|
||||
const customHeadingStyles = css`
|
||||
.custom-heading {
|
||||
|
||||
@@ -163,7 +163,13 @@ const usePreviewExtensions = () => {
|
||||
const enableAI = useEnableAI();
|
||||
|
||||
const extensions = useMemo(() => {
|
||||
const manager = getViewManager(framework, enableAI);
|
||||
const manager = getViewManager()
|
||||
.config.init()
|
||||
.common(framework, enableAI)
|
||||
.theme(framework)
|
||||
.database(framework)
|
||||
.linkedDoc(framework)
|
||||
.paragraph(enableAI).value;
|
||||
const specs = manager.get('preview-page');
|
||||
return [...specs, patchReferenceRenderer(reactToLit, referenceRenderer)];
|
||||
}, [reactToLit, referenceRenderer, framework, enableAI]);
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
LitEdgelessEditor,
|
||||
type PageEditor,
|
||||
} from '@affine/core/blocksuite/editors';
|
||||
import type { AffineEditorViewOptions } from '@affine/core/blocksuite/manager/editor-view';
|
||||
import { getViewManager } from '@affine/core/blocksuite/manager/migrating-view';
|
||||
import { useEnableAI } from '@affine/core/components/hooks/affine/use-enable-ai';
|
||||
import type { DocCustomPropertyInfo } from '@affine/core/modules/db';
|
||||
@@ -22,9 +21,8 @@ import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import track from '@affine/track';
|
||||
import type { DocTitle } from '@blocksuite/affine/fragments/doc-title';
|
||||
import type { DocMode } from '@blocksuite/affine/model';
|
||||
import type { ExtensionType, Store } from '@blocksuite/affine/store';
|
||||
import type { Store } from '@blocksuite/affine/store';
|
||||
import {
|
||||
type FrameworkProvider,
|
||||
useFramework,
|
||||
useLiveData,
|
||||
useService,
|
||||
@@ -69,7 +67,7 @@ const usePatchSpecs = (mode: DocMode) => {
|
||||
|
||||
const enableAI = useEnableAI();
|
||||
|
||||
const insidePeekView = useInsidePeekView();
|
||||
const isInPeekView = useInsidePeekView();
|
||||
|
||||
const enableTurboRenderer = useLiveData(
|
||||
featureFlagService.flags.enable_turbo_renderer.$
|
||||
@@ -81,33 +79,51 @@ const usePatchSpecs = (mode: DocMode) => {
|
||||
)
|
||||
);
|
||||
|
||||
const editorOptions: AffineEditorViewOptions = useMemo(() => {
|
||||
return {
|
||||
isCloud,
|
||||
isInPeekView: insidePeekView,
|
||||
const patchedSpecs = useMemo(() => {
|
||||
const manager = getViewManager()
|
||||
.config.init()
|
||||
.common(framework, enableAI)
|
||||
.theme(framework)
|
||||
.editorConfig(framework)
|
||||
.editorView({
|
||||
isCloud,
|
||||
isInPeekView,
|
||||
enableTurboRenderer,
|
||||
enablePDFEmbedPreview,
|
||||
framework,
|
||||
reactToLit,
|
||||
confirmModal,
|
||||
})
|
||||
.edgelessBlockHeader({
|
||||
framework,
|
||||
isInPeekView,
|
||||
reactToLit,
|
||||
})
|
||||
.database(framework)
|
||||
.linkedDoc(framework)
|
||||
.paragraph(enableAI).value;
|
||||
|
||||
enableTurboRenderer,
|
||||
enablePDFEmbedPreview,
|
||||
|
||||
framework,
|
||||
|
||||
reactToLit: reactToLit as AffineEditorViewOptions['reactToLit'],
|
||||
confirmModal,
|
||||
};
|
||||
if (BUILD_CONFIG.isMobileEdition) {
|
||||
if (mode === 'page') {
|
||||
return manager.get('mobile-page');
|
||||
} else {
|
||||
return manager.get('mobile-edgeless');
|
||||
}
|
||||
} else {
|
||||
return manager.get(mode);
|
||||
}
|
||||
}, [
|
||||
confirmModal,
|
||||
enableAI,
|
||||
enablePDFEmbedPreview,
|
||||
enableTurboRenderer,
|
||||
framework,
|
||||
insidePeekView,
|
||||
isInPeekView,
|
||||
isCloud,
|
||||
mode,
|
||||
reactToLit,
|
||||
]);
|
||||
|
||||
const patchedSpecs = useMemo(() => {
|
||||
return enableEditorExtension(framework, mode, enableAI, editorOptions);
|
||||
}, [framework, mode, enableAI, editorOptions]);
|
||||
|
||||
return [
|
||||
patchedSpecs,
|
||||
useMemo(
|
||||
@@ -302,20 +318,3 @@ export const BlocksuiteEdgelessEditor = forwardRef<
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
function enableEditorExtension(
|
||||
framework: FrameworkProvider,
|
||||
mode: 'edgeless' | 'page',
|
||||
enableAI: boolean,
|
||||
options: AffineEditorViewOptions
|
||||
): ExtensionType[] {
|
||||
const manager = getViewManager(framework, enableAI, options);
|
||||
if (BUILD_CONFIG.isMobileEdition) {
|
||||
if (mode === 'page') {
|
||||
return manager.get('mobile-page');
|
||||
}
|
||||
|
||||
return manager.get('mobile-edgeless');
|
||||
}
|
||||
return manager.get(mode);
|
||||
}
|
||||
|
||||
@@ -1,110 +1,42 @@
|
||||
import type { ReactToLit } from '@affine/component';
|
||||
import { JournalService } from '@affine/core/modules/journal';
|
||||
import { EmbedSyncedDocConfigExtension } from '@blocksuite/affine/blocks/embed-doc';
|
||||
import { NoteConfigExtension } from '@blocksuite/affine/blocks/note';
|
||||
import { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine/blocks/root';
|
||||
import { Bound, Vec } from '@blocksuite/affine/global/gfx';
|
||||
import type { ElementOrFactory } from '@affine/component';
|
||||
import {
|
||||
DocModeProvider,
|
||||
EditPropsStore,
|
||||
} from '@blocksuite/affine/shared/services';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/affine/std/gfx';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
import { html } from 'lit';
|
||||
patchForEdgelessNoteConfig,
|
||||
patchForEmbedSyncedDocConfig,
|
||||
} from '@affine/core/blocksuite/extensions/edgeless-block-header/patch';
|
||||
import {
|
||||
type ViewExtensionContext,
|
||||
ViewExtensionProvider,
|
||||
} from '@blocksuite/affine/ext-loader';
|
||||
import { FrameworkProvider } from '@toeverything/infra';
|
||||
import type { TemplateResult } from 'lit';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { BlocksuiteEditorJournalDocTitle } from '../../block-suite-editor/journal-doc-title';
|
||||
import { EdgelessEmbedSyncedDocHeader } from './edgeless-embed-synced-doc-header';
|
||||
import { EdgelessNoteHeader } from './edgeless-note-header';
|
||||
const optionsSchema = z.object({
|
||||
isInPeekView: z.boolean(),
|
||||
framework: z.instanceof(FrameworkProvider),
|
||||
reactToLit: z
|
||||
.function()
|
||||
.args(z.custom<ElementOrFactory>(), z.boolean().optional())
|
||||
.returns(z.custom<TemplateResult>()),
|
||||
});
|
||||
|
||||
export function patchForEdgelessNoteConfig(
|
||||
framework: FrameworkProvider,
|
||||
reactToLit: ReactToLit,
|
||||
insidePeekView: boolean
|
||||
) {
|
||||
return NoteConfigExtension({
|
||||
edgelessNoteHeader: ({ note }) =>
|
||||
reactToLit(<EdgelessNoteHeader note={note} />),
|
||||
pageBlockTitle: ({ note }) => {
|
||||
const journalService = framework.get(JournalService);
|
||||
const isJournal = !!journalService.journalDate$(note.store.id).value;
|
||||
if (isJournal) {
|
||||
return reactToLit(
|
||||
<BlocksuiteEditorJournalDocTitle page={note.store} />
|
||||
);
|
||||
} else {
|
||||
return html`<doc-title .doc=${note.store}></doc-title>`;
|
||||
}
|
||||
},
|
||||
pageBlockViewportFitAnimation: insidePeekView
|
||||
? undefined
|
||||
: ({ std, note }) => {
|
||||
const storedViewport = std.get(EditPropsStore).getStorage('viewport');
|
||||
// if there is a stored viewport, don't run the animation
|
||||
// in other word, this doc has been opened before
|
||||
if (storedViewport) return false;
|
||||
export type EdgelessBlockHeaderViewOptions = z.infer<typeof optionsSchema>;
|
||||
|
||||
if (!std.store.root) return false;
|
||||
const rootView = std.view.getBlock(std.store.root.id);
|
||||
if (!rootView) return false;
|
||||
export class EdgelessBlockHeaderConfigViewExtension extends ViewExtensionProvider<EdgelessBlockHeaderViewOptions> {
|
||||
override name = 'header-config-view';
|
||||
override schema = optionsSchema;
|
||||
|
||||
const gfx = std.get(GfxControllerIdentifier);
|
||||
const primaryMode = std
|
||||
.get(DocModeProvider)
|
||||
.getPrimaryMode(std.store.id);
|
||||
override setup(
|
||||
context: ViewExtensionContext,
|
||||
options?: EdgelessBlockHeaderViewOptions
|
||||
) {
|
||||
super.setup(context, options);
|
||||
if (!options) return;
|
||||
const { framework, isInPeekView, reactToLit } = options;
|
||||
|
||||
if (primaryMode !== 'page' || !note || note.props.edgeless.collapse) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const leftPadding = parseInt(
|
||||
window
|
||||
.getComputedStyle(rootView)
|
||||
.getPropertyValue('--affine-editor-side-padding')
|
||||
.replace('px', '')
|
||||
);
|
||||
if (isNaN(leftPadding)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let editorWidth = parseInt(
|
||||
window
|
||||
.getComputedStyle(rootView)
|
||||
.getPropertyValue('--affine-editor-width')
|
||||
.replace('px', '')
|
||||
);
|
||||
if (isNaN(editorWidth)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const containerWidth = rootView.getBoundingClientRect().width;
|
||||
const leftMargin =
|
||||
containerWidth > editorWidth
|
||||
? (containerWidth - editorWidth) / 2
|
||||
: 0;
|
||||
|
||||
const pageTitleAnchor = gfx.viewport.toModelCoord(
|
||||
leftPadding + leftMargin,
|
||||
0
|
||||
);
|
||||
|
||||
const noteBound = Bound.deserialize(note.xywh);
|
||||
const edgelessTitleAnchor = Vec.add(noteBound.tl, [
|
||||
EDGELESS_BLOCK_CHILD_PADDING,
|
||||
12,
|
||||
]);
|
||||
|
||||
const center = Vec.sub(edgelessTitleAnchor, pageTitleAnchor);
|
||||
gfx.viewport.setCenter(center[0], center[1]);
|
||||
gfx.viewport.smoothZoom(0.65, undefined, 15);
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function patchForEmbedSyncedDocConfig(reactToLit: ReactToLit) {
|
||||
return EmbedSyncedDocConfigExtension({
|
||||
edgelessHeader: ({ model, std }) =>
|
||||
reactToLit(<EdgelessEmbedSyncedDocHeader model={model} std={std} />),
|
||||
});
|
||||
context.register(
|
||||
patchForEdgelessNoteConfig(framework, reactToLit, isInPeekView)
|
||||
);
|
||||
context.register(patchForEmbedSyncedDocConfig(reactToLit));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import type { ReactToLit } from '@affine/component';
|
||||
import { JournalService } from '@affine/core/modules/journal';
|
||||
import { EmbedSyncedDocConfigExtension } from '@blocksuite/affine/blocks/embed-doc';
|
||||
import { NoteConfigExtension } from '@blocksuite/affine/blocks/note';
|
||||
import { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine/blocks/root';
|
||||
import { Bound, Vec } from '@blocksuite/affine/global/gfx';
|
||||
import {
|
||||
DocModeProvider,
|
||||
EditPropsStore,
|
||||
} from '@blocksuite/affine/shared/services';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/affine/std/gfx';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
import { html } from 'lit';
|
||||
|
||||
import { BlocksuiteEditorJournalDocTitle } from '../../block-suite-editor/journal-doc-title';
|
||||
import { EdgelessEmbedSyncedDocHeader } from './edgeless-embed-synced-doc-header';
|
||||
import { EdgelessNoteHeader } from './edgeless-note-header';
|
||||
|
||||
export function patchForEdgelessNoteConfig(
|
||||
framework: FrameworkProvider,
|
||||
reactToLit: ReactToLit,
|
||||
insidePeekView: boolean
|
||||
) {
|
||||
return NoteConfigExtension({
|
||||
edgelessNoteHeader: ({ note }) =>
|
||||
reactToLit(<EdgelessNoteHeader note={note} />),
|
||||
pageBlockTitle: ({ note }) => {
|
||||
const journalService = framework.get(JournalService);
|
||||
const isJournal = !!journalService.journalDate$(note.store.id).value;
|
||||
if (isJournal) {
|
||||
return reactToLit(
|
||||
<BlocksuiteEditorJournalDocTitle page={note.store} />
|
||||
);
|
||||
} else {
|
||||
return html`<doc-title .doc=${note.store}></doc-title>`;
|
||||
}
|
||||
},
|
||||
pageBlockViewportFitAnimation: insidePeekView
|
||||
? undefined
|
||||
: ({ std, note }) => {
|
||||
const storedViewport = std.get(EditPropsStore).getStorage('viewport');
|
||||
// if there is a stored viewport, don't run the animation
|
||||
// in other word, this doc has been opened before
|
||||
if (storedViewport) return false;
|
||||
|
||||
if (!std.store.root) return false;
|
||||
const rootView = std.view.getBlock(std.store.root.id);
|
||||
if (!rootView) return false;
|
||||
|
||||
const gfx = std.get(GfxControllerIdentifier);
|
||||
const primaryMode = std
|
||||
.get(DocModeProvider)
|
||||
.getPrimaryMode(std.store.id);
|
||||
|
||||
if (primaryMode !== 'page' || !note || note.props.edgeless.collapse) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const leftPadding = parseInt(
|
||||
window
|
||||
.getComputedStyle(rootView)
|
||||
.getPropertyValue('--affine-editor-side-padding')
|
||||
.replace('px', '')
|
||||
);
|
||||
if (isNaN(leftPadding)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let editorWidth = parseInt(
|
||||
window
|
||||
.getComputedStyle(rootView)
|
||||
.getPropertyValue('--affine-editor-width')
|
||||
.replace('px', '')
|
||||
);
|
||||
if (isNaN(editorWidth)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const containerWidth = rootView.getBoundingClientRect().width;
|
||||
const leftMargin =
|
||||
containerWidth > editorWidth
|
||||
? (containerWidth - editorWidth) / 2
|
||||
: 0;
|
||||
|
||||
const pageTitleAnchor = gfx.viewport.toModelCoord(
|
||||
leftPadding + leftMargin,
|
||||
0
|
||||
);
|
||||
|
||||
const noteBound = Bound.deserialize(note.xywh);
|
||||
const edgelessTitleAnchor = Vec.add(noteBound.tl, [
|
||||
EDGELESS_BLOCK_CHILD_PADDING,
|
||||
12,
|
||||
]);
|
||||
|
||||
const center = Vec.sub(edgelessTitleAnchor, pageTitleAnchor);
|
||||
gfx.viewport.setCenter(center[0], center[1]);
|
||||
gfx.viewport.smoothZoom(0.65, undefined, 15);
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function patchForEmbedSyncedDocConfig(reactToLit: ReactToLit) {
|
||||
return EmbedSyncedDocConfigExtension({
|
||||
edgelessHeader: ({ model, std }) =>
|
||||
reactToLit(<EdgelessEmbedSyncedDocHeader model={model} std={std} />),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
createCustomToolbarExtension,
|
||||
createToolbarMoreMenuConfig,
|
||||
} from '@affine/core/blocksuite/extensions/editor-config/toolbar';
|
||||
import { WorkspaceServerService } from '@affine/core/modules/cloud';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { ToolbarMoreMenuConfigExtension } from '@blocksuite/affine/components/toolbar';
|
||||
import { EditorSettingExtension } from '@blocksuite/affine/shared/services';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
export function getEditorConfigExtension(
|
||||
framework: FrameworkProvider
|
||||
): ExtensionType[] {
|
||||
const editorSettingService = framework.get(EditorSettingService);
|
||||
const workspaceServerService = framework.get(WorkspaceServerService);
|
||||
const baseUrl = workspaceServerService.server?.baseUrl ?? location.origin;
|
||||
|
||||
return [
|
||||
EditorSettingExtension({
|
||||
// eslint-disable-next-line rxjs/finnish
|
||||
setting$: editorSettingService.editorSetting.settingSignal,
|
||||
set: (k, v) => editorSettingService.editorSetting.set(k, v),
|
||||
}),
|
||||
ToolbarMoreMenuConfigExtension(createToolbarMoreMenuConfig(framework)),
|
||||
|
||||
createCustomToolbarExtension(editorSettingService.editorSetting, baseUrl),
|
||||
].flat();
|
||||
}
|
||||
@@ -1,30 +1,34 @@
|
||||
import { WorkspaceServerService } from '@affine/core/modules/cloud';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { ToolbarMoreMenuConfigExtension } from '@blocksuite/affine/components/toolbar';
|
||||
import { EditorSettingExtension } from '@blocksuite/affine/shared/services';
|
||||
import type { ExtensionType } from '@blocksuite/affine/store';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
import { getEditorConfigExtension } from '@affine/core/blocksuite/extensions/editor-config/get-config';
|
||||
import {
|
||||
createCustomToolbarExtension,
|
||||
createToolbarMoreMenuConfig,
|
||||
} from './toolbar';
|
||||
type ViewExtensionContext,
|
||||
ViewExtensionProvider,
|
||||
} from '@blocksuite/affine/ext-loader';
|
||||
import { FrameworkProvider } from '@toeverything/infra';
|
||||
import { z } from 'zod';
|
||||
|
||||
export function getEditorConfigExtension(
|
||||
framework: FrameworkProvider
|
||||
): ExtensionType[] {
|
||||
const editorSettingService = framework.get(EditorSettingService);
|
||||
const workspaceServerService = framework.get(WorkspaceServerService);
|
||||
const baseUrl = workspaceServerService.server?.baseUrl ?? location.origin;
|
||||
const optionsSchema = z.object({
|
||||
framework: z.instanceof(FrameworkProvider).optional(),
|
||||
});
|
||||
|
||||
return [
|
||||
EditorSettingExtension({
|
||||
// eslint-disable-next-line rxjs/finnish
|
||||
setting$: editorSettingService.editorSetting.settingSignal,
|
||||
set: (k, v) => editorSettingService.editorSetting.set(k, v),
|
||||
}),
|
||||
ToolbarMoreMenuConfigExtension(createToolbarMoreMenuConfig(framework)),
|
||||
type AffineEditorConfigViewOptions = z.infer<typeof optionsSchema>;
|
||||
|
||||
createCustomToolbarExtension(editorSettingService.editorSetting, baseUrl),
|
||||
].flat();
|
||||
export class AffineEditorConfigViewExtension extends ViewExtensionProvider<AffineEditorConfigViewOptions> {
|
||||
override name = 'affine-view-editor-config';
|
||||
|
||||
override schema = optionsSchema;
|
||||
|
||||
override setup(
|
||||
context: ViewExtensionContext,
|
||||
options?: AffineEditorConfigViewOptions
|
||||
) {
|
||||
super.setup(context, options);
|
||||
const framework = options?.framework;
|
||||
if (!framework) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.scope === 'edgeless' || context.scope === 'page') {
|
||||
context.register(getEditorConfigExtension(framework));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { getPreviewThemeExtension } from '@affine/core/blocksuite/extensions/theme/preview-theme';
|
||||
import { getThemeExtension } from '@affine/core/blocksuite/extensions/theme/theme';
|
||||
import {
|
||||
type ViewExtensionContext,
|
||||
ViewExtensionProvider,
|
||||
} from '@blocksuite/affine/ext-loader';
|
||||
import { FrameworkProvider } from '@toeverything/infra';
|
||||
import { z } from 'zod';
|
||||
|
||||
const optionsSchema = z.object({
|
||||
framework: z.instanceof(FrameworkProvider).optional(),
|
||||
});
|
||||
|
||||
type AffineThemeViewOptions = z.infer<typeof optionsSchema>;
|
||||
|
||||
export class AffineThemeViewExtension extends ViewExtensionProvider<AffineThemeViewOptions> {
|
||||
override name = 'affine-view-theme';
|
||||
|
||||
override schema = optionsSchema;
|
||||
|
||||
override setup(
|
||||
context: ViewExtensionContext,
|
||||
options?: AffineThemeViewOptions
|
||||
) {
|
||||
super.setup(context, options);
|
||||
const framework = options?.framework;
|
||||
if (!framework) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isPreview(context.scope)) {
|
||||
context.register(getPreviewThemeExtension(framework));
|
||||
} else {
|
||||
context.register(getThemeExtension(framework));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { AppThemeService } from '@affine/core/modules/theme';
|
||||
import { ColorScheme } from '@blocksuite/affine/model';
|
||||
import {
|
||||
type ThemeExtension,
|
||||
ThemeExtensionIdentifier,
|
||||
} from '@blocksuite/affine/shared/services';
|
||||
import {
|
||||
createSignalFromObservable,
|
||||
type Signal,
|
||||
} from '@blocksuite/affine/shared/utils';
|
||||
import {
|
||||
type BlockStdScope,
|
||||
LifeCycleWatcher,
|
||||
StdIdentifier,
|
||||
} from '@blocksuite/affine/std';
|
||||
import type { Container } from '@blocksuite/global/di';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
import type { Observable } from 'rxjs';
|
||||
|
||||
export function getPreviewThemeExtension(framework: FrameworkProvider) {
|
||||
class AffinePagePreviewThemeExtension
|
||||
extends LifeCycleWatcher
|
||||
implements ThemeExtension
|
||||
{
|
||||
static override readonly key = 'affine-page-preview-theme';
|
||||
|
||||
readonly theme: Signal<ColorScheme>;
|
||||
|
||||
readonly disposables: (() => void)[] = [];
|
||||
|
||||
static override setup(di: Container) {
|
||||
super.setup(di);
|
||||
di.override(ThemeExtensionIdentifier, AffinePagePreviewThemeExtension, [
|
||||
StdIdentifier,
|
||||
]);
|
||||
}
|
||||
|
||||
constructor(std: BlockStdScope) {
|
||||
super(std);
|
||||
const theme$: Observable<ColorScheme> = framework
|
||||
.get(AppThemeService)
|
||||
.appTheme.theme$.map(theme => {
|
||||
return theme === ColorScheme.Dark
|
||||
? ColorScheme.Dark
|
||||
: ColorScheme.Light;
|
||||
});
|
||||
const { signal, cleanup } = createSignalFromObservable<ColorScheme>(
|
||||
theme$,
|
||||
ColorScheme.Light
|
||||
);
|
||||
this.theme = signal;
|
||||
this.disposables.push(cleanup);
|
||||
}
|
||||
|
||||
getAppTheme() {
|
||||
return this.theme;
|
||||
}
|
||||
|
||||
getEdgelessTheme() {
|
||||
return this.theme;
|
||||
}
|
||||
|
||||
override unmounted() {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposables.forEach(dispose => dispose());
|
||||
}
|
||||
}
|
||||
|
||||
return AffinePagePreviewThemeExtension;
|
||||
}
|
||||
@@ -10,11 +10,7 @@ import {
|
||||
createSignalFromObservable,
|
||||
type Signal,
|
||||
} from '@blocksuite/affine/shared/utils';
|
||||
import {
|
||||
type BlockStdScope,
|
||||
LifeCycleWatcher,
|
||||
StdIdentifier,
|
||||
} from '@blocksuite/affine/std';
|
||||
import { LifeCycleWatcher, StdIdentifier } from '@blocksuite/affine/std';
|
||||
import { type FrameworkProvider } from '@toeverything/infra';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { combineLatest, map } from 'rxjs';
|
||||
@@ -99,58 +95,3 @@ export function getThemeExtension(
|
||||
|
||||
return AffineThemeExtension;
|
||||
}
|
||||
|
||||
export function getPreviewThemeExtension(framework: FrameworkProvider) {
|
||||
class AffinePagePreviewThemeExtension
|
||||
extends LifeCycleWatcher
|
||||
implements ThemeExtension
|
||||
{
|
||||
static override readonly key = 'affine-page-preview-theme';
|
||||
|
||||
readonly theme: Signal<ColorScheme>;
|
||||
|
||||
readonly disposables: (() => void)[] = [];
|
||||
|
||||
static override setup(di: Container) {
|
||||
super.setup(di);
|
||||
di.override(ThemeExtensionIdentifier, AffinePagePreviewThemeExtension, [
|
||||
StdIdentifier,
|
||||
]);
|
||||
}
|
||||
|
||||
constructor(std: BlockStdScope) {
|
||||
super(std);
|
||||
const theme$: Observable<ColorScheme> = framework
|
||||
.get(AppThemeService)
|
||||
.appTheme.theme$.map(theme => {
|
||||
return theme === ColorScheme.Dark
|
||||
? ColorScheme.Dark
|
||||
: ColorScheme.Light;
|
||||
});
|
||||
const { signal, cleanup } = createSignalFromObservable<ColorScheme>(
|
||||
theme$,
|
||||
ColorScheme.Light
|
||||
);
|
||||
this.theme = signal;
|
||||
this.disposables.push(cleanup);
|
||||
}
|
||||
|
||||
getAppTheme() {
|
||||
return this.theme;
|
||||
}
|
||||
|
||||
getEdgelessTheme() {
|
||||
return this.theme;
|
||||
}
|
||||
|
||||
override unmounted() {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposables.forEach(dispose => dispose());
|
||||
}
|
||||
}
|
||||
|
||||
return AffinePagePreviewThemeExtension;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
effects as htmlPreviewEffects,
|
||||
} from '../extensions/code-block-preview/html-preview';
|
||||
|
||||
export class CodeBlockPreviewExtensionProvider extends ViewExtensionProvider {
|
||||
export class CodeBlockPreviewViewExtension extends ViewExtensionProvider {
|
||||
override name = 'code-block-preview';
|
||||
|
||||
override effect() {
|
||||
|
||||
@@ -11,15 +11,10 @@ import { CopilotTool } from '@affine/core/blocksuite/ai/tool/copilot-tool';
|
||||
import { aiPanelWidget } from '@affine/core/blocksuite/ai/widgets/ai-panel/ai-panel';
|
||||
import { edgelessCopilotWidget } from '@affine/core/blocksuite/ai/widgets/edgeless-copilot';
|
||||
import { buildDocDisplayMetaExtension } from '@affine/core/blocksuite/extensions/display-meta';
|
||||
import { getEditorConfigExtension } from '@affine/core/blocksuite/extensions/editor-config';
|
||||
import { patchFileSizeLimitExtension } from '@affine/core/blocksuite/extensions/file-size-limit';
|
||||
import { getFontConfigExtension } from '@affine/core/blocksuite/extensions/font-config';
|
||||
import { patchPeekViewService } from '@affine/core/blocksuite/extensions/peek-view-service';
|
||||
import { getTelemetryExtension } from '@affine/core/blocksuite/extensions/telemetry';
|
||||
import {
|
||||
getPreviewThemeExtension,
|
||||
getThemeExtension,
|
||||
} from '@affine/core/blocksuite/extensions/theme';
|
||||
import { PeekViewService } from '@affine/core/modules/peek-view';
|
||||
import {
|
||||
type ViewExtensionContext,
|
||||
@@ -42,17 +37,6 @@ export class AffineCommonViewExtension extends ViewExtensionProvider<
|
||||
|
||||
override schema = optionsSchema;
|
||||
|
||||
private _setupTheme(
|
||||
context: ViewExtensionContext,
|
||||
framework: FrameworkProvider
|
||||
) {
|
||||
if (this.isPreview(context.scope)) {
|
||||
context.register(getPreviewThemeExtension(framework));
|
||||
} else {
|
||||
context.register(getThemeExtension(framework));
|
||||
}
|
||||
}
|
||||
|
||||
private _setupAI(
|
||||
context: ViewExtensionContext,
|
||||
framework: FrameworkProvider
|
||||
@@ -97,7 +81,7 @@ export class AffineCommonViewExtension extends ViewExtensionProvider<
|
||||
context: ViewExtensionContext,
|
||||
options?: z.infer<typeof optionsSchema>
|
||||
) {
|
||||
super.setup(context);
|
||||
super.setup(context, options);
|
||||
const { framework, enableAI } = options || {};
|
||||
if (framework) {
|
||||
context.register(patchPeekViewService(framework.get(PeekViewService)));
|
||||
@@ -106,9 +90,7 @@ export class AffineCommonViewExtension extends ViewExtensionProvider<
|
||||
buildDocDisplayMetaExtension(framework),
|
||||
]);
|
||||
context.register(getTelemetryExtension());
|
||||
this._setupTheme(context, framework);
|
||||
if (context.scope === 'edgeless' || context.scope === 'page') {
|
||||
context.register(getEditorConfigExtension(framework));
|
||||
context.register(patchFileSizeLimitExtension(framework));
|
||||
}
|
||||
if (enableAI) {
|
||||
|
||||
@@ -6,10 +6,6 @@ import {
|
||||
import { patchDatabaseBlockConfigService } from '@affine/core/blocksuite/extensions/database-block-config-service';
|
||||
import { patchDocModeService } from '@affine/core/blocksuite/extensions/doc-mode-service';
|
||||
import { patchDocUrlExtensions } from '@affine/core/blocksuite/extensions/doc-url';
|
||||
import {
|
||||
patchForEdgelessNoteConfig,
|
||||
patchForEmbedSyncedDocConfig,
|
||||
} from '@affine/core/blocksuite/extensions/edgeless-block-header';
|
||||
import { EdgelessClipboardAIChatConfig } from '@affine/core/blocksuite/extensions/edgeless-clipboard';
|
||||
import { patchForClipboardInElectron } from '@affine/core/blocksuite/extensions/electron-clipboard';
|
||||
import { patchNotificationService } from '@affine/core/blocksuite/extensions/notification-service';
|
||||
@@ -116,13 +112,12 @@ export class AffineEditorViewExtension extends ViewExtensionProvider<AffineEdito
|
||||
context: ViewExtensionContext,
|
||||
options?: AffineEditorViewOptions
|
||||
) {
|
||||
super.setup(context);
|
||||
super.setup(context, options);
|
||||
if (!options) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
isCloud,
|
||||
isInPeekView,
|
||||
|
||||
enableTurboRenderer,
|
||||
enablePDFEmbedPreview,
|
||||
@@ -155,8 +150,6 @@ export class AffineEditorViewExtension extends ViewExtensionProvider<AffineEdito
|
||||
context.register(patchDocUrlExtensions(framework));
|
||||
context.register(patchQuickSearchService(framework));
|
||||
context.register([
|
||||
patchForEmbedSyncedDocConfig(reactToLit),
|
||||
patchForEdgelessNoteConfig(framework, reactToLit, isInPeekView),
|
||||
patchDatabaseBlockConfigService(),
|
||||
patchForAudioEmbedView(reactToLit),
|
||||
]);
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import {
|
||||
EdgelessBlockHeaderConfigViewExtension,
|
||||
type EdgelessBlockHeaderViewOptions,
|
||||
} from '@affine/core/blocksuite/extensions/edgeless-block-header';
|
||||
import { AffineEditorConfigViewExtension } from '@affine/core/blocksuite/extensions/editor-config';
|
||||
import { createDatabaseOptionsConfig } from '@affine/core/blocksuite/extensions/editor-config/database';
|
||||
import { createLinkedWidgetConfig } from '@affine/core/blocksuite/extensions/editor-config/linked';
|
||||
import { AffineThemeViewExtension } from '@affine/core/blocksuite/extensions/theme';
|
||||
import { AffineCommonViewExtension } from '@affine/core/blocksuite/manager/common-view';
|
||||
import {
|
||||
AffineEditorViewExtension,
|
||||
@@ -12,54 +18,142 @@ import { getInternalViewExtensions } from '@blocksuite/affine/extensions/view';
|
||||
import { LinkedDocViewExtension } from '@blocksuite/affine/widgets/linked-doc/view';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
import { CodeBlockPreviewExtensionProvider } from './code-block-preview';
|
||||
import { CodeBlockPreviewViewExtension } from './code-block-preview';
|
||||
|
||||
const manager = new ViewExtensionManager([
|
||||
...getInternalViewExtensions(),
|
||||
|
||||
AffineCommonViewExtension,
|
||||
AffineEditorViewExtension,
|
||||
CodeBlockPreviewExtensionProvider,
|
||||
]);
|
||||
|
||||
export function getViewManager(
|
||||
framework?: FrameworkProvider,
|
||||
enableAI?: boolean,
|
||||
options?: AffineEditorViewOptions
|
||||
) {
|
||||
manager.configure(AffineCommonViewExtension, {
|
||||
framework,
|
||||
enableAI,
|
||||
});
|
||||
manager.configure(AffineEditorViewExtension, options);
|
||||
|
||||
if (framework) {
|
||||
manager.configure(
|
||||
DatabaseViewExtension,
|
||||
createDatabaseOptionsConfig(framework)
|
||||
);
|
||||
manager.configure(
|
||||
LinkedDocViewExtension,
|
||||
createLinkedWidgetConfig(framework)
|
||||
);
|
||||
class ViewProvider {
|
||||
static instance: ViewProvider | null = null;
|
||||
static getInstance() {
|
||||
if (!ViewProvider.instance) {
|
||||
ViewProvider.instance = new ViewProvider();
|
||||
}
|
||||
return ViewProvider.instance;
|
||||
}
|
||||
|
||||
if (enableAI) {
|
||||
manager.configure(ParagraphViewExtension, {
|
||||
getPlaceholder: model => {
|
||||
const placeholders = {
|
||||
text: "Type '/' for commands, 'space' for AI",
|
||||
h1: 'Heading 1',
|
||||
h2: 'Heading 2',
|
||||
h3: 'Heading 3',
|
||||
h4: 'Heading 4',
|
||||
h5: 'Heading 5',
|
||||
h6: 'Heading 6',
|
||||
quote: '',
|
||||
};
|
||||
return placeholders[model.props.type] ?? '';
|
||||
},
|
||||
private readonly _manager: ViewExtensionManager;
|
||||
|
||||
constructor() {
|
||||
this._manager = new ViewExtensionManager([
|
||||
...getInternalViewExtensions(),
|
||||
|
||||
AffineThemeViewExtension,
|
||||
AffineCommonViewExtension,
|
||||
AffineEditorViewExtension,
|
||||
AffineEditorConfigViewExtension,
|
||||
CodeBlockPreviewViewExtension,
|
||||
EdgelessBlockHeaderConfigViewExtension,
|
||||
]);
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this._manager;
|
||||
}
|
||||
|
||||
get config() {
|
||||
return {
|
||||
init: this._initDefaultConfig,
|
||||
common: this._configureCommon,
|
||||
editorView: this._configureEditorView,
|
||||
theme: this._configureTheme,
|
||||
editorConfig: this._configureEditorConfig,
|
||||
edgelessBlockHeader: this._configureEdgelessBlockHeader,
|
||||
database: this._configureDatabase,
|
||||
linkedDoc: this._configureLinkedDoc,
|
||||
paragraph: this._configureParagraph,
|
||||
value: this._manager,
|
||||
};
|
||||
}
|
||||
|
||||
private readonly _initDefaultConfig = () => {
|
||||
this.config
|
||||
.common()
|
||||
.theme()
|
||||
.editorView()
|
||||
.editorConfig()
|
||||
.edgelessBlockHeader()
|
||||
.database()
|
||||
.linkedDoc()
|
||||
.paragraph();
|
||||
|
||||
return this.config;
|
||||
};
|
||||
|
||||
private readonly _configureCommon = (
|
||||
framework?: FrameworkProvider,
|
||||
enableAI?: boolean
|
||||
) => {
|
||||
this._manager.configure(AffineCommonViewExtension, {
|
||||
framework,
|
||||
enableAI,
|
||||
});
|
||||
}
|
||||
return manager;
|
||||
return this.config;
|
||||
};
|
||||
|
||||
private readonly _configureEditorView = (
|
||||
options?: AffineEditorViewOptions
|
||||
) => {
|
||||
this._manager.configure(AffineEditorViewExtension, options);
|
||||
return this.config;
|
||||
};
|
||||
|
||||
private readonly _configureTheme = (framework?: FrameworkProvider) => {
|
||||
this._manager.configure(AffineThemeViewExtension, { framework });
|
||||
return this.config;
|
||||
};
|
||||
|
||||
private readonly _configureEditorConfig = (framework?: FrameworkProvider) => {
|
||||
this._manager.configure(AffineEditorConfigViewExtension, { framework });
|
||||
return this.config;
|
||||
};
|
||||
|
||||
private readonly _configureEdgelessBlockHeader = (
|
||||
options?: EdgelessBlockHeaderViewOptions
|
||||
) => {
|
||||
this._manager.configure(EdgelessBlockHeaderConfigViewExtension, options);
|
||||
return this.config;
|
||||
};
|
||||
|
||||
private readonly _configureDatabase = (framework?: FrameworkProvider) => {
|
||||
if (framework) {
|
||||
this._manager.configure(
|
||||
DatabaseViewExtension,
|
||||
createDatabaseOptionsConfig(framework)
|
||||
);
|
||||
}
|
||||
return this.config;
|
||||
};
|
||||
|
||||
private readonly _configureLinkedDoc = (framework?: FrameworkProvider) => {
|
||||
if (framework) {
|
||||
this._manager.configure(
|
||||
LinkedDocViewExtension,
|
||||
createLinkedWidgetConfig(framework)
|
||||
);
|
||||
}
|
||||
return this.config;
|
||||
};
|
||||
|
||||
private readonly _configureParagraph = (enableAI?: boolean) => {
|
||||
if (enableAI) {
|
||||
this._manager.configure(ParagraphViewExtension, {
|
||||
getPlaceholder: model => {
|
||||
const placeholders = {
|
||||
text: "Type '/' for commands, 'space' for AI",
|
||||
h1: 'Heading 1',
|
||||
h2: 'Heading 2',
|
||||
h3: 'Heading 3',
|
||||
h4: 'Heading 4',
|
||||
h5: 'Heading 5',
|
||||
h6: 'Heading 6',
|
||||
quote: '',
|
||||
};
|
||||
return placeholders[model.props.type] ?? '';
|
||||
},
|
||||
});
|
||||
}
|
||||
return this.config;
|
||||
};
|
||||
}
|
||||
|
||||
export function getViewManager() {
|
||||
return ViewProvider.getInstance();
|
||||
}
|
||||
|
||||
@@ -56,7 +56,12 @@ export const EdgelessSnapshot = (props: Props) => {
|
||||
const { editorSetting } = framework.get(EditorSettingService);
|
||||
|
||||
const extensions = useMemo(() => {
|
||||
const manager = getViewManager(framework, false);
|
||||
const manager = getViewManager()
|
||||
.config.init()
|
||||
.common(framework)
|
||||
.theme(framework)
|
||||
.database(framework)
|
||||
.linkedDoc(framework).value;
|
||||
return manager
|
||||
.get('preview-edgeless')
|
||||
.concat([ViewportElementExtension('.ref-viewport')]);
|
||||
|
||||
@@ -10,7 +10,7 @@ export function createBlockStdScope(doc: Store) {
|
||||
logger.debug('createBlockStdScope', doc.id);
|
||||
const std = new BlockStdScope({
|
||||
store: doc,
|
||||
extensions: getViewManager().get('page'),
|
||||
extensions: getViewManager().config.init().value.get('page'),
|
||||
});
|
||||
return std;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user