mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-17 01:56:27 +08:00
refactor(editor): modular custom specs (#10398)
Key Changes: 1. **Removal of Scroll Anchoring Widget** - Removed the scroll anchoring widget import and its related implementation from `blocksuite/affine/block-root/src/common-specs/widgets.ts` 2. **Enhanced React-Lit Integration** - Added `ReactWebComponent` type export in `packages/frontend/component/src/lit-react/index.ts` - Refactored text renderer component to use React integration: - Added React import and created `LitTextRenderer` component using `createReactComponentFromLit` - Moved the component declaration to a more appropriate location 3. **AI Feature Flag Integration** - Added feature flag check for AI functionality in `enableAIExtension` - Only enables AI extensions if the `enable_ai` flag is true 4. **Component Restructuring** - Moved several components and utilities to dedicated extension files - Consolidated Lit adapter implementations - Removed direct widget imports in favor of extension-based approach - Reorganized editor component structure for better maintainability 5. **File Reorganization** - Removed `specs/custom/spec-patchers.ts` and distributed its functionality across multiple extension files - Created new extension files for various features like: - Attachment embed views - Doc mode service - Doc URL handling - Edgeless clipboard - Mobile support - Note configuration - Various service patches (notification, peek view, quick search, etc.) 6. **Mobile Support Improvements** - Refactored mobile extension enablement to be more modular - Moved mobile-specific widget omissions into a dedicated extension 7. **Type System Improvements** - Added more specific type imports for editors and components - Enhanced type safety across the codebase This PR appears to be a significant refactoring effort focused on: 1. Improving code organization through better separation of concerns 2. Enhancing the integration between React and Lit components 3. Adding feature flag support for AI capabilities 4. Making the codebase more maintainable and modular 5. Improving mobile support 6. Strengthening type safety The changes suggest a move towards a more extension-based architecture, where functionality is more clearly separated into distinct modules rather than being centralized in larger files.
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import { AFFINE_SCROLL_ANCHORING_WIDGET } from '@blocksuite/affine-widget-scroll-anchoring';
|
||||
import { WidgetViewExtension } from '@blocksuite/block-std';
|
||||
import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||
|
||||
@@ -45,8 +44,3 @@ export const viewportOverlayWidget = WidgetViewExtension(
|
||||
AFFINE_VIEWPORT_OVERLAY_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_VIEWPORT_OVERLAY_WIDGET)}`
|
||||
);
|
||||
export const scrollAnchoringWidget = WidgetViewExtension(
|
||||
'affine:page',
|
||||
AFFINE_SCROLL_ANCHORING_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_SCROLL_ANCHORING_WIDGET)}`
|
||||
);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export { createComponent as createReactComponentFromLit } from './create-component';
|
||||
export {
|
||||
createComponent as createReactComponentFromLit,
|
||||
type ReactWebComponent,
|
||||
} from './create-component';
|
||||
export * from './lit-portal';
|
||||
export { toReactNode } from './to-react-node';
|
||||
export { templateToString } from './utils';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createReactComponentFromLit } from '@affine/component';
|
||||
import {
|
||||
BlockStdScope,
|
||||
BlockViewIdentifier,
|
||||
@@ -30,6 +31,7 @@ import { property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { keyed } from 'lit/directives/keyed.js';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
import React from 'react';
|
||||
|
||||
import { markDownToDoc } from '../../utils';
|
||||
import type {
|
||||
@@ -349,12 +351,6 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
|
||||
accessor state: AffineAIPanelState | undefined = undefined;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'text-renderer': TextRenderer;
|
||||
}
|
||||
}
|
||||
|
||||
export const createTextRenderer: (
|
||||
host: EditorHost,
|
||||
options: TextRendererOptions
|
||||
@@ -368,3 +364,14 @@ export const createTextRenderer: (
|
||||
></text-renderer>`;
|
||||
};
|
||||
};
|
||||
|
||||
export const LitTextRenderer = createReactComponentFromLit({
|
||||
react: React,
|
||||
elementClass: TextRenderer,
|
||||
});
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'text-renderer': TextRenderer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import {
|
||||
CodeBlockSpec,
|
||||
EdgelessRootBlockSpec,
|
||||
@@ -19,6 +20,12 @@ export function enableAIExtension(
|
||||
specBuilder: SpecBuilder,
|
||||
framework: FrameworkProvider
|
||||
) {
|
||||
const featureFlagService = framework.get(FeatureFlagService);
|
||||
const enableAI = featureFlagService.flags.enable_ai.value;
|
||||
if (!enableAI) {
|
||||
return;
|
||||
}
|
||||
|
||||
specBuilder.replace(CodeBlockSpec, AICodeBlockSpec);
|
||||
specBuilder.replace(ImageBlockSpec, AIImageBlockSpec);
|
||||
specBuilder.replace(ParagraphBlockSpec, AIParagraphBlockSpec);
|
||||
|
||||
+6
-16
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
Button,
|
||||
createReactComponentFromLit,
|
||||
Divider,
|
||||
useLitPortalFactory,
|
||||
} from '@affine/component';
|
||||
import { Button, Divider, useLitPortalFactory } from '@affine/component';
|
||||
import { DocService } from '@affine/core/modules/doc';
|
||||
import {
|
||||
type Backlink,
|
||||
@@ -28,7 +23,7 @@ import {
|
||||
useLiveData,
|
||||
useServices,
|
||||
} from '@toeverything/infra';
|
||||
import React, {
|
||||
import {
|
||||
Fragment,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
@@ -40,19 +35,14 @@ import {
|
||||
AffinePageReference,
|
||||
AffineSharedPageReference,
|
||||
} from '../../components/affine/reference-link';
|
||||
import { TextRenderer } from '../ai/components/text-renderer';
|
||||
import * as styles from './bi-directional-link-panel.css';
|
||||
import { LitTextRenderer } from '../ai/components/text-renderer';
|
||||
import {
|
||||
patchReferenceRenderer,
|
||||
type ReferenceReactRenderer,
|
||||
} from './specs/custom/spec-patchers';
|
||||
} from '../extensions/reference-renderer';
|
||||
import * as styles from './bi-directional-link-panel.css';
|
||||
import { createPageModeSpecs } from './specs/page';
|
||||
|
||||
const BlocksuiteTextRenderer = createReactComponentFromLit({
|
||||
react: React,
|
||||
elementClass: TextRenderer,
|
||||
});
|
||||
|
||||
const PREFIX = 'bi-directional-link-panel-collapse:';
|
||||
|
||||
const useBiDirectionalLinkPanelCollapseState = (
|
||||
@@ -288,7 +278,7 @@ export const BacklinkGroups = () => {
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<BlocksuiteTextRenderer
|
||||
<LitTextRenderer
|
||||
className={styles.linkPreviewRenderer}
|
||||
answer={link.markdownPreview}
|
||||
schema={getAFFiNEWorkspaceSchema()}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useConfirmModal, useLitPortalFactory } from '@affine/component';
|
||||
import {
|
||||
createReactComponentFromLit,
|
||||
useConfirmModal,
|
||||
useLitPortalFactory,
|
||||
} from '@affine/component';
|
||||
import { EdgelessEditor, PageEditor } from '@affine/core/blocksuite/editors';
|
||||
type EdgelessEditor,
|
||||
LitDocEditor,
|
||||
LitDocTitle,
|
||||
LitEdgelessEditor,
|
||||
type PageEditor,
|
||||
} from '@affine/core/blocksuite/editors';
|
||||
import type { DocCustomPropertyInfo } from '@affine/core/modules/db';
|
||||
import { DocService, DocsService } from '@affine/core/modules/doc';
|
||||
import type {
|
||||
@@ -18,16 +20,7 @@ import { toURLSearchParams } from '@affine/core/modules/navigation';
|
||||
import { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import track from '@affine/track';
|
||||
import {
|
||||
codeToolbarWidget,
|
||||
type DocMode,
|
||||
DocTitle,
|
||||
embedCardToolbarWidget,
|
||||
formatBarWidget,
|
||||
imageToolbarWidget,
|
||||
slashMenuWidget,
|
||||
surfaceRefToolbarWidget,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import type { DocMode, DocTitle } from '@blocksuite/affine/blocks';
|
||||
import type { Store } from '@blocksuite/affine/store';
|
||||
import {
|
||||
useFramework,
|
||||
@@ -35,7 +28,8 @@ import {
|
||||
useService,
|
||||
useServices,
|
||||
} from '@toeverything/infra';
|
||||
import React, {
|
||||
import type React from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
Fragment,
|
||||
useCallback,
|
||||
@@ -52,46 +46,30 @@ import {
|
||||
type DefaultOpenProperty,
|
||||
DocPropertiesTable,
|
||||
} from '../../components/doc-properties';
|
||||
import { patchForAttachmentEmbedViews } from '../extensions/attachment-embed-view';
|
||||
import { patchDocModeService } from '../extensions/doc-mode-service';
|
||||
import { patchDocUrlExtensions } from '../extensions/doc-url';
|
||||
import { EdgelessClipboardWatcher } from '../extensions/edgeless-clipboard';
|
||||
import { patchForClipboardInElectron } from '../extensions/electron-clipboard';
|
||||
import { enableMobileExtension } from '../extensions/entry/enable-mobile';
|
||||
import { patchForEdgelessNoteConfig } from '../extensions/note-config';
|
||||
import { patchNotificationService } from '../extensions/notification-service';
|
||||
import { patchOpenDocExtension } from '../extensions/open-doc';
|
||||
import { patchPeekViewService } from '../extensions/peek-view-service';
|
||||
import { patchQuickSearchService } from '../extensions/quick-search-service';
|
||||
import {
|
||||
patchReferenceRenderer,
|
||||
type ReferenceReactRenderer,
|
||||
} from '../extensions/reference-renderer';
|
||||
import { patchSideBarService } from '../extensions/side-bar-service';
|
||||
import { BiDirectionalLinkPanel } from './bi-directional-link-panel';
|
||||
import { BlocksuiteEditorJournalDocTitle } from './journal-doc-title';
|
||||
import { extendEdgelessPreviewSpec } from './specs/custom/root-block';
|
||||
import {
|
||||
patchDocModeService,
|
||||
patchEdgelessClipboard,
|
||||
patchForAttachmentEmbedViews,
|
||||
patchForClipboardInElectron,
|
||||
patchForEdgelessNoteConfig,
|
||||
patchForMobile,
|
||||
patchGenerateDocUrlExtension,
|
||||
patchNotificationService,
|
||||
patchOpenDocExtension,
|
||||
patchParseDocUrlExtension,
|
||||
patchPeekViewService,
|
||||
patchQuickSearchService,
|
||||
patchReferenceRenderer,
|
||||
patchSideBarService,
|
||||
type ReferenceReactRenderer,
|
||||
} from './specs/custom/spec-patchers';
|
||||
import { createEdgelessModeSpecs } from './specs/edgeless';
|
||||
import { createPageModeSpecs } from './specs/page';
|
||||
import { extendEdgelessPreviewSpec } from './specs/preview';
|
||||
import { StarterBar } from './starter-bar';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
const adapted = {
|
||||
DocEditor: createReactComponentFromLit({
|
||||
react: React,
|
||||
elementClass: PageEditor,
|
||||
}),
|
||||
DocTitle: createReactComponentFromLit({
|
||||
react: React,
|
||||
elementClass: DocTitle,
|
||||
}),
|
||||
EdgelessEditor: createReactComponentFromLit({
|
||||
react: React,
|
||||
elementClass: EdgelessEditor,
|
||||
}),
|
||||
};
|
||||
|
||||
interface BlocksuiteEditorProps {
|
||||
page: Store;
|
||||
readonly?: boolean;
|
||||
@@ -165,9 +143,8 @@ const usePatchSpecs = (mode: DocMode) => {
|
||||
patchNotificationService(confirmModal),
|
||||
patchPeekViewService(peekViewService),
|
||||
patchOpenDocExtension(),
|
||||
patchEdgelessClipboard(),
|
||||
patchParseDocUrlExtension(framework),
|
||||
patchGenerateDocUrlExtension(framework),
|
||||
EdgelessClipboardWatcher,
|
||||
patchDocUrlExtensions(framework),
|
||||
patchQuickSearchService(framework),
|
||||
patchSideBarService(framework),
|
||||
patchDocModeService(docService, docsService, editorService),
|
||||
@@ -178,13 +155,7 @@ const usePatchSpecs = (mode: DocMode) => {
|
||||
builder.extend([patchForAttachmentEmbedViews(reactToLit)]);
|
||||
}
|
||||
if (BUILD_CONFIG.isMobileEdition) {
|
||||
builder.omit(formatBarWidget);
|
||||
builder.omit(embedCardToolbarWidget);
|
||||
builder.omit(slashMenuWidget);
|
||||
builder.omit(codeToolbarWidget);
|
||||
builder.omit(imageToolbarWidget);
|
||||
builder.omit(surfaceRefToolbarWidget);
|
||||
builder.extend([patchForMobile()].flat());
|
||||
enableMobileExtension(builder);
|
||||
}
|
||||
if (BUILD_CONFIG.isElectron) {
|
||||
builder.extend([patchForClipboardInElectron(framework)].flat());
|
||||
@@ -319,7 +290,7 @@ export const BlocksuiteDocEditor = forwardRef<
|
||||
<>
|
||||
<div className={styles.affineDocViewport}>
|
||||
{!isJournal ? (
|
||||
<adapted.DocTitle doc={page} ref={onTitleRef} />
|
||||
<LitDocTitle doc={page} ref={onTitleRef} />
|
||||
) : (
|
||||
<BlocksuiteEditorJournalDocTitle page={page} />
|
||||
)}
|
||||
@@ -335,7 +306,7 @@ export const BlocksuiteDocEditor = forwardRef<
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<adapted.DocEditor
|
||||
<LitDocEditor
|
||||
className={styles.docContainer}
|
||||
ref={onDocRef}
|
||||
doc={page}
|
||||
@@ -391,7 +362,7 @@ export const BlocksuiteEdgelessEditor = forwardRef<
|
||||
|
||||
return (
|
||||
<div className={styles.affineEdgelessDocViewport}>
|
||||
<adapted.EdgelessEditor ref={onDocRef} doc={page} specs={specs} />
|
||||
<LitEdgelessEditor ref={onDocRef} doc={page} specs={specs} />
|
||||
{portals}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
import { DocService, DocsService } from '@affine/core/modules/doc';
|
||||
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { AppThemeService } from '@affine/core/modules/theme';
|
||||
import { mixpanel } from '@affine/track';
|
||||
import { LifeCycleWatcher, StdIdentifier } from '@blocksuite/affine/block-std';
|
||||
import type {
|
||||
DocDisplayMetaExtension,
|
||||
DocDisplayMetaParams,
|
||||
Signal,
|
||||
SpecBuilder,
|
||||
TelemetryEventMap,
|
||||
ThemeExtension,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import {
|
||||
ColorScheme,
|
||||
createSignalFromObservable,
|
||||
DatabaseConfigExtension,
|
||||
DocDisplayMetaProvider,
|
||||
EditorSettingExtension,
|
||||
referenceToNode,
|
||||
RootBlockConfigExtension,
|
||||
SpecProvider,
|
||||
TelemetryProvider,
|
||||
ThemeExtensionIdentifier,
|
||||
ToolbarMoreMenuConfigExtension,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import type { Container } from '@blocksuite/affine/global/di';
|
||||
import type { ExtensionType } from '@blocksuite/affine/store';
|
||||
import { LinkedPageIcon, PageIcon } from '@blocksuite/icons/lit';
|
||||
import { type FrameworkProvider } from '@toeverything/infra';
|
||||
import type { TemplateResult } from 'lit';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { combineLatest, map } from 'rxjs';
|
||||
|
||||
import { getFontConfigExtension } from '../font-extension';
|
||||
import { createDatabaseOptionsConfig } from './database-block';
|
||||
import { createLinkedWidgetConfig } from './widgets/linked';
|
||||
import { createToolbarMoreMenuConfig } from './widgets/toolbar';
|
||||
|
||||
function getTelemetryExtension(): ExtensionType {
|
||||
return {
|
||||
setup: di => {
|
||||
di.addImpl(TelemetryProvider, () => ({
|
||||
track: <T extends keyof TelemetryEventMap>(
|
||||
eventName: T,
|
||||
props: TelemetryEventMap[T]
|
||||
) => {
|
||||
mixpanel.track(eventName as string, props as Record<string, unknown>);
|
||||
},
|
||||
}));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getThemeExtension(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 || 'system'
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
export function buildDocDisplayMetaExtension(framework: FrameworkProvider) {
|
||||
const docDisplayMetaService = framework.get(DocDisplayMetaService);
|
||||
|
||||
function iconBuilder(
|
||||
icon: typeof PageIcon,
|
||||
size = '1.25em',
|
||||
style = 'user-select:none;flex-shrink:0;vertical-align:middle;font-size:inherit;margin-bottom:0.1em;'
|
||||
) {
|
||||
return icon({
|
||||
width: size,
|
||||
height: size,
|
||||
style,
|
||||
});
|
||||
}
|
||||
|
||||
class AffineDocDisplayMetaService
|
||||
extends LifeCycleWatcher
|
||||
implements DocDisplayMetaExtension
|
||||
{
|
||||
static override key = 'doc-display-meta';
|
||||
|
||||
readonly disposables: (() => void)[] = [];
|
||||
|
||||
static override setup(di: Container) {
|
||||
super.setup(di);
|
||||
di.override(DocDisplayMetaProvider, this, [StdIdentifier]);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
while (this.disposables.length > 0) {
|
||||
this.disposables.pop()?.();
|
||||
}
|
||||
}
|
||||
|
||||
icon(
|
||||
docId: string,
|
||||
{ params, title, referenced }: DocDisplayMetaParams = {}
|
||||
): Signal<TemplateResult> {
|
||||
const icon$ = docDisplayMetaService
|
||||
.icon$(docId, {
|
||||
type: 'lit',
|
||||
title,
|
||||
reference: referenced,
|
||||
referenceToNode: referenceToNode({ pageId: docId, params }),
|
||||
})
|
||||
.map(iconBuilder);
|
||||
|
||||
const { signal: iconSignal, cleanup } = createSignalFromObservable(
|
||||
icon$,
|
||||
iconBuilder(referenced ? LinkedPageIcon : PageIcon)
|
||||
);
|
||||
|
||||
this.disposables.push(cleanup);
|
||||
|
||||
return iconSignal;
|
||||
}
|
||||
|
||||
title(
|
||||
docId: string,
|
||||
{ title, referenced }: DocDisplayMetaParams = {}
|
||||
): Signal<string> {
|
||||
const title$ = docDisplayMetaService.title$(docId, {
|
||||
title,
|
||||
reference: referenced,
|
||||
});
|
||||
|
||||
const { signal: titleSignal, cleanup } =
|
||||
createSignalFromObservable<string>(title$, title ?? '');
|
||||
|
||||
this.disposables.push(cleanup);
|
||||
|
||||
return titleSignal;
|
||||
}
|
||||
|
||||
override unmounted() {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
return AffineDocDisplayMetaService;
|
||||
}
|
||||
|
||||
function getEditorConfigExtension(
|
||||
framework: FrameworkProvider
|
||||
): ExtensionType[] {
|
||||
const editorSettingService = framework.get(EditorSettingService);
|
||||
return [
|
||||
EditorSettingExtension(editorSettingService.editorSetting.settingSignal),
|
||||
DatabaseConfigExtension(createDatabaseOptionsConfig(framework)),
|
||||
RootBlockConfigExtension({
|
||||
linkedWidget: createLinkedWidgetConfig(framework),
|
||||
}),
|
||||
ToolbarMoreMenuConfigExtension(createToolbarMoreMenuConfig(framework)),
|
||||
];
|
||||
}
|
||||
|
||||
export const extendEdgelessPreviewSpec = (function () {
|
||||
let _extension: ExtensionType;
|
||||
let _framework: FrameworkProvider;
|
||||
return function (framework: FrameworkProvider) {
|
||||
if (framework === _framework && _extension) {
|
||||
return _extension;
|
||||
} else {
|
||||
_extension && SpecProvider._.omitSpec('preview:edgeless', _extension);
|
||||
_extension = getThemeExtension(framework);
|
||||
_framework = framework;
|
||||
SpecProvider._.extendSpec('preview:edgeless', [_extension]);
|
||||
return _extension;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
export function enableAffineExtension(
|
||||
framework: FrameworkProvider,
|
||||
specBuilder: SpecBuilder
|
||||
): void {
|
||||
specBuilder.extend(
|
||||
[
|
||||
getThemeExtension(framework),
|
||||
getFontConfigExtension(),
|
||||
getTelemetryExtension(),
|
||||
getEditorConfigExtension(framework),
|
||||
buildDocDisplayMetaExtension(framework),
|
||||
].flat()
|
||||
);
|
||||
}
|
||||
-761
@@ -1,761 +0,0 @@
|
||||
import {
|
||||
type ElementOrFactory,
|
||||
Input,
|
||||
notify,
|
||||
toast,
|
||||
type ToastOptions,
|
||||
toReactNode,
|
||||
type useConfirmModal,
|
||||
} from '@affine/component';
|
||||
import { AIChatBlockSchema } from '@affine/core/blocksuite/ai/blocks';
|
||||
import { WorkspaceServerService } from '@affine/core/modules/cloud';
|
||||
import { DesktopApiService } from '@affine/core/modules/desktop-api';
|
||||
import { type DocService, DocsService } from '@affine/core/modules/doc';
|
||||
import type { EditorService } from '@affine/core/modules/editor';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { JournalService } from '@affine/core/modules/journal';
|
||||
import { resolveLinkToDoc } from '@affine/core/modules/navigation';
|
||||
import type { PeekViewService } from '@affine/core/modules/peek-view';
|
||||
import {
|
||||
CreationQuickSearchSession,
|
||||
DocsQuickSearchSession,
|
||||
LinksQuickSearchSession,
|
||||
QuickSearchService,
|
||||
RecentDocsQuickSearchSession,
|
||||
} from '@affine/core/modules/quicksearch';
|
||||
import { ExternalLinksQuickSearchSession } from '@affine/core/modules/quicksearch/impls/external-links';
|
||||
import { JournalsQuickSearchSession } from '@affine/core/modules/quicksearch/impls/journals';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { I18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import {
|
||||
BlockServiceWatcher,
|
||||
type BlockStdScope,
|
||||
BlockViewIdentifier,
|
||||
ConfigIdentifier,
|
||||
LifeCycleWatcher,
|
||||
type WidgetComponent,
|
||||
} from '@blocksuite/affine/block-std';
|
||||
import type {
|
||||
AffineReference,
|
||||
CodeBlockConfig,
|
||||
DocMode,
|
||||
DocModeProvider,
|
||||
OpenDocConfig,
|
||||
OpenDocConfigItem,
|
||||
PeekOptions,
|
||||
PeekViewService as BSPeekViewService,
|
||||
QuickSearchResult,
|
||||
ReferenceNodeConfig,
|
||||
RootBlockConfig,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import {
|
||||
AffineSlashMenuWidget,
|
||||
AttachmentEmbedConfigIdentifier,
|
||||
DocModeExtension,
|
||||
EdgelessRootBlockComponent,
|
||||
EmbedLinkedDocBlockComponent,
|
||||
FeatureFlagService,
|
||||
GenerateDocUrlExtension,
|
||||
insertLinkByQuickSearchCommand,
|
||||
NativeClipboardExtension,
|
||||
NoteConfigExtension,
|
||||
NotificationExtension,
|
||||
OpenDocExtension,
|
||||
ParagraphBlockService,
|
||||
ParseDocUrlExtension,
|
||||
PeekViewExtension,
|
||||
QuickSearchExtension,
|
||||
ReferenceNodeConfigExtension,
|
||||
ReferenceNodeConfigIdentifier,
|
||||
RootBlockConfigExtension,
|
||||
SidebarExtension,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import type { Container } from '@blocksuite/affine/global/di';
|
||||
import { Bound } from '@blocksuite/affine/global/utils';
|
||||
import {
|
||||
type BlockSnapshot,
|
||||
type ExtensionType,
|
||||
Text,
|
||||
} from '@blocksuite/affine/store';
|
||||
import type { ReferenceParams } from '@blocksuite/affine-model';
|
||||
import {
|
||||
CenterPeekIcon,
|
||||
ExpandFullIcon,
|
||||
OpenInNewIcon,
|
||||
SplitViewIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { type FrameworkProvider } from '@toeverything/infra';
|
||||
import { html, type TemplateResult } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
import { pick } from 'lodash-es';
|
||||
|
||||
import { AttachmentEmbedPreview } from '../../../../components/attachment-viewer/pdf-viewer-embedded';
|
||||
import { generateUrl } from '../../../../components/hooks/affine/use-share-url';
|
||||
import type { DocProps } from '../../../initialization';
|
||||
import { BlocksuiteEditorJournalDocTitle } from '../../journal-doc-title';
|
||||
import { EdgelessNoteHeader } from './widgets/edgeless-note-header';
|
||||
import { createKeyboardToolbarConfig } from './widgets/keyboard-toolbar';
|
||||
|
||||
export type ReferenceReactRenderer = (
|
||||
reference: AffineReference
|
||||
) => React.ReactElement;
|
||||
|
||||
const logger = new DebugLogger('affine::spec-patchers');
|
||||
|
||||
function patchSpecService(
|
||||
flavour: string,
|
||||
onWidgetConnected?: (component: WidgetComponent) => void
|
||||
) {
|
||||
class TempServiceWatcher extends BlockServiceWatcher {
|
||||
static override readonly flavour = flavour;
|
||||
override mounted() {
|
||||
super.mounted();
|
||||
const disposableGroup = this.blockService.disposables;
|
||||
if (onWidgetConnected) {
|
||||
disposableGroup.add(
|
||||
this.blockService.specSlots.widgetConnected.on(({ component }) => {
|
||||
onWidgetConnected(component);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return TempServiceWatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch the block specs with custom renderers.
|
||||
*/
|
||||
export function patchReferenceRenderer(
|
||||
reactToLit: (element: ElementOrFactory) => TemplateResult,
|
||||
reactRenderer: ReferenceReactRenderer
|
||||
): ExtensionType {
|
||||
const customContent = (reference: AffineReference) => {
|
||||
const node = reactRenderer(reference);
|
||||
return reactToLit(node);
|
||||
};
|
||||
|
||||
return ReferenceNodeConfigExtension({
|
||||
customContent,
|
||||
});
|
||||
}
|
||||
|
||||
export function patchNotificationService({
|
||||
closeConfirmModal,
|
||||
openConfirmModal,
|
||||
}: ReturnType<typeof useConfirmModal>) {
|
||||
return NotificationExtension({
|
||||
confirm: async ({ title, message, confirmText, cancelText, abort }) => {
|
||||
return new Promise<boolean>(resolve => {
|
||||
openConfirmModal({
|
||||
title: toReactNode(title),
|
||||
description: toReactNode(message),
|
||||
confirmText,
|
||||
confirmButtonOptions: {
|
||||
variant: 'primary',
|
||||
},
|
||||
cancelText,
|
||||
onConfirm: () => {
|
||||
resolve(true);
|
||||
},
|
||||
onCancel: () => {
|
||||
resolve(false);
|
||||
},
|
||||
});
|
||||
abort?.addEventListener('abort', () => {
|
||||
resolve(false);
|
||||
closeConfirmModal();
|
||||
});
|
||||
});
|
||||
},
|
||||
prompt: async ({
|
||||
title,
|
||||
message,
|
||||
confirmText,
|
||||
placeholder,
|
||||
cancelText,
|
||||
autofill,
|
||||
abort,
|
||||
}) => {
|
||||
return new Promise<string | null>(resolve => {
|
||||
let value = autofill || '';
|
||||
const description = (
|
||||
<div>
|
||||
<span style={{ marginBottom: 12 }}>{toReactNode(message)}</span>
|
||||
<Input
|
||||
autoSelect={true}
|
||||
placeholder={placeholder}
|
||||
defaultValue={value}
|
||||
onChange={e => (value = e)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
openConfirmModal({
|
||||
title: toReactNode(title),
|
||||
description: description,
|
||||
confirmText: confirmText ?? 'Confirm',
|
||||
confirmButtonOptions: {
|
||||
variant: 'primary',
|
||||
},
|
||||
cancelText: cancelText ?? 'Cancel',
|
||||
onConfirm: () => {
|
||||
resolve(value);
|
||||
},
|
||||
onCancel: () => {
|
||||
resolve(null);
|
||||
},
|
||||
autoFocusConfirm: false,
|
||||
});
|
||||
abort?.addEventListener('abort', () => {
|
||||
resolve(null);
|
||||
closeConfirmModal();
|
||||
});
|
||||
});
|
||||
},
|
||||
toast: (message: string, options: ToastOptions) => {
|
||||
return toast(message, options);
|
||||
},
|
||||
notify: notification => {
|
||||
const accentToNotify = {
|
||||
error: notify.error,
|
||||
success: notify.success,
|
||||
warning: notify.warning,
|
||||
info: notify,
|
||||
};
|
||||
|
||||
const fn = accentToNotify[notification.accent || 'info'];
|
||||
if (!fn) {
|
||||
throw new Error('Invalid notification accent');
|
||||
}
|
||||
|
||||
const toastId = fn(
|
||||
{
|
||||
title: toReactNode(notification.title),
|
||||
message: toReactNode(notification.message),
|
||||
footer: toReactNode(notification.footer),
|
||||
action: notification.action?.onClick
|
||||
? {
|
||||
label: toReactNode(notification.action?.label),
|
||||
onClick: notification.action.onClick,
|
||||
}
|
||||
: undefined,
|
||||
onDismiss: notification.onClose,
|
||||
},
|
||||
{
|
||||
duration: notification.duration || 0,
|
||||
onDismiss: notification.onClose,
|
||||
onAutoClose: notification.onClose,
|
||||
}
|
||||
);
|
||||
|
||||
notification.abort?.addEventListener('abort', () => {
|
||||
notify.dismiss(toastId);
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function patchOpenDocExtension() {
|
||||
const openDocConfig: OpenDocConfig = {
|
||||
items: [
|
||||
{
|
||||
type: 'open-in-active-view',
|
||||
label: I18n['com.affine.peek-view-controls.open-doc'](),
|
||||
icon: ExpandFullIcon(),
|
||||
},
|
||||
BUILD_CONFIG.isElectron
|
||||
? {
|
||||
type: 'open-in-new-view',
|
||||
label:
|
||||
I18n['com.affine.peek-view-controls.open-doc-in-split-view'](),
|
||||
icon: SplitViewIcon(),
|
||||
}
|
||||
: null,
|
||||
{
|
||||
type: 'open-in-new-tab',
|
||||
label: I18n['com.affine.peek-view-controls.open-doc-in-new-tab'](),
|
||||
icon: OpenInNewIcon(),
|
||||
},
|
||||
{
|
||||
type: 'open-in-center-peek',
|
||||
label: I18n['com.affine.peek-view-controls.open-doc-in-center-peek'](),
|
||||
icon: CenterPeekIcon(),
|
||||
},
|
||||
].filter((item): item is OpenDocConfigItem => item !== null),
|
||||
};
|
||||
return OpenDocExtension(openDocConfig);
|
||||
}
|
||||
|
||||
export function patchPeekViewService(service: PeekViewService) {
|
||||
return PeekViewExtension({
|
||||
peek: (
|
||||
element: {
|
||||
target: HTMLElement;
|
||||
docId: string;
|
||||
blockIds?: string[];
|
||||
template?: TemplateResult;
|
||||
},
|
||||
options?: PeekOptions
|
||||
) => {
|
||||
logger.debug('center peek', element);
|
||||
const { template, target, ...props } = element;
|
||||
|
||||
return service.peekView.open(
|
||||
{
|
||||
element: target,
|
||||
docRef: props,
|
||||
},
|
||||
template,
|
||||
options?.abortSignal
|
||||
);
|
||||
},
|
||||
} satisfies BSPeekViewService);
|
||||
}
|
||||
|
||||
export function patchDocModeService(
|
||||
docService: DocService,
|
||||
docsService: DocsService,
|
||||
editorService: EditorService
|
||||
): ExtensionType {
|
||||
const DEFAULT_MODE = 'page';
|
||||
class AffineDocModeService implements DocModeProvider {
|
||||
setEditorMode = (mode: DocMode) => {
|
||||
editorService.editor.setMode(mode);
|
||||
};
|
||||
getEditorMode = () => {
|
||||
return editorService.editor.mode$.value;
|
||||
};
|
||||
setPrimaryMode = (mode: DocMode, id?: string) => {
|
||||
if (id) {
|
||||
docsService.list.setPrimaryMode(id, mode);
|
||||
} else {
|
||||
docService.doc.setPrimaryMode(mode);
|
||||
}
|
||||
};
|
||||
getPrimaryMode = (id?: string) => {
|
||||
const mode = id
|
||||
? docsService.list.getPrimaryMode(id)
|
||||
: docService.doc.getPrimaryMode();
|
||||
return (mode || DEFAULT_MODE) as DocMode;
|
||||
};
|
||||
togglePrimaryMode = (id?: string) => {
|
||||
const mode = id
|
||||
? docsService.list.togglePrimaryMode(id)
|
||||
: docService.doc.togglePrimaryMode();
|
||||
return (mode || DEFAULT_MODE) as DocMode;
|
||||
};
|
||||
onPrimaryModeChange = (handler: (mode: DocMode) => void, id?: string) => {
|
||||
const mode$ = id
|
||||
? docsService.list.primaryMode$(id)
|
||||
: docService.doc.primaryMode$;
|
||||
const sub = mode$.subscribe(m => handler((m || DEFAULT_MODE) as DocMode));
|
||||
return {
|
||||
dispose: sub.unsubscribe,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const docModeExtension = DocModeExtension(new AffineDocModeService());
|
||||
|
||||
return docModeExtension;
|
||||
}
|
||||
|
||||
export function patchQuickSearchService(framework: FrameworkProvider) {
|
||||
const QuickSearch = QuickSearchExtension({
|
||||
async openQuickSearch() {
|
||||
let searchResult: QuickSearchResult = null;
|
||||
searchResult = await new Promise((resolve, reject) =>
|
||||
framework.get(QuickSearchService).quickSearch.show(
|
||||
[
|
||||
framework.get(RecentDocsQuickSearchSession),
|
||||
framework.get(CreationQuickSearchSession),
|
||||
framework.get(DocsQuickSearchSession),
|
||||
framework.get(LinksQuickSearchSession),
|
||||
framework.get(ExternalLinksQuickSearchSession),
|
||||
framework.get(JournalsQuickSearchSession),
|
||||
],
|
||||
result => {
|
||||
if (result === null) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.source === 'docs') {
|
||||
resolve({
|
||||
docId: result.payload.docId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.source === 'recent-doc') {
|
||||
resolve({
|
||||
docId: result.payload.docId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.source === 'link') {
|
||||
resolve({
|
||||
docId: result.payload.docId,
|
||||
params: pick(result.payload, [
|
||||
'mode',
|
||||
'blockIds',
|
||||
'elementIds',
|
||||
]),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.source === 'date-picker') {
|
||||
result.payload
|
||||
.getDocId()
|
||||
.then(docId => {
|
||||
if (docId) {
|
||||
resolve({ docId });
|
||||
}
|
||||
})
|
||||
.catch(reject);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.source === 'external-link') {
|
||||
const externalUrl = result.payload.url;
|
||||
resolve({ externalUrl });
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.source === 'creation') {
|
||||
const docsService = framework.get(DocsService);
|
||||
const editorSettingService = framework.get(EditorSettingService);
|
||||
const mode =
|
||||
result.id === 'creation:create-edgeless' ? 'edgeless' : 'page';
|
||||
const docProps: DocProps = {
|
||||
page: { title: new Text(result.payload.title) },
|
||||
note: editorSettingService.editorSetting.get('affine:note'),
|
||||
};
|
||||
const newDoc = docsService.createDoc({
|
||||
primaryMode: mode,
|
||||
docProps,
|
||||
});
|
||||
|
||||
resolve({ docId: newDoc.id });
|
||||
return;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: {
|
||||
i18nKey: 'com.affine.cmdk.insert-links',
|
||||
},
|
||||
placeholder: {
|
||||
i18nKey: 'com.affine.cmdk.docs.placeholder',
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return searchResult;
|
||||
},
|
||||
});
|
||||
|
||||
const SlashMenuQuickSearchExtension = patchSpecService(
|
||||
'affine:page',
|
||||
(component: WidgetComponent) => {
|
||||
if (component instanceof AffineSlashMenuWidget) {
|
||||
component.config.items.forEach(item => {
|
||||
if (
|
||||
'action' in item &&
|
||||
(item.name === 'Linked Doc' || item.name === 'Link')
|
||||
) {
|
||||
item.action = async ({ rootComponent }) => {
|
||||
const [success, { insertedLinkType }] =
|
||||
rootComponent.std.command.exec(insertLinkByQuickSearchCommand);
|
||||
|
||||
if (!success) return;
|
||||
|
||||
insertedLinkType
|
||||
?.then(type => {
|
||||
const flavour = type?.flavour;
|
||||
if (!flavour) return;
|
||||
|
||||
if (flavour === 'affine:bookmark') {
|
||||
track.doc.editor.slashMenu.bookmark();
|
||||
return;
|
||||
}
|
||||
|
||||
if (flavour === 'affine:embed-linked-doc') {
|
||||
track.doc.editor.slashMenu.linkDoc({
|
||||
control: 'linkDoc',
|
||||
});
|
||||
return;
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
return [QuickSearch, SlashMenuQuickSearchExtension];
|
||||
}
|
||||
|
||||
export function patchParseDocUrlExtension(framework: FrameworkProvider) {
|
||||
const workspaceService = framework.get(WorkspaceService);
|
||||
const ParseDocUrl = ParseDocUrlExtension({
|
||||
parseDocUrl(url) {
|
||||
const info = resolveLinkToDoc(url);
|
||||
if (!info || info.workspaceId !== workspaceService.workspace.id) return;
|
||||
|
||||
delete info.refreshKey;
|
||||
|
||||
return info;
|
||||
},
|
||||
});
|
||||
|
||||
return [ParseDocUrl];
|
||||
}
|
||||
|
||||
export function patchGenerateDocUrlExtension(framework: FrameworkProvider) {
|
||||
const workspaceService = framework.get(WorkspaceService);
|
||||
const workspaceServerService = framework.get(WorkspaceServerService);
|
||||
const GenerateDocUrl = GenerateDocUrlExtension({
|
||||
generateDocUrl(pageId: string, params?: ReferenceParams) {
|
||||
return generateUrl({
|
||||
...params,
|
||||
pageId,
|
||||
workspaceId: workspaceService.workspace.id,
|
||||
baseUrl: workspaceServerService.server?.baseUrl ?? location.origin,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return [GenerateDocUrl];
|
||||
}
|
||||
|
||||
export function patchEdgelessClipboard() {
|
||||
class EdgelessClipboardWatcher extends BlockServiceWatcher {
|
||||
static override readonly flavour = 'affine:page';
|
||||
|
||||
override mounted() {
|
||||
super.mounted();
|
||||
this.blockService.disposables.add(
|
||||
this.blockService.specSlots.viewConnected.on(view => {
|
||||
const { component } = view;
|
||||
if (component instanceof EdgelessRootBlockComponent) {
|
||||
const AIChatBlockFlavour = AIChatBlockSchema.model.flavour;
|
||||
const createFunc = (block: BlockSnapshot) => {
|
||||
const {
|
||||
xywh,
|
||||
scale,
|
||||
messages,
|
||||
sessionId,
|
||||
rootDocId,
|
||||
rootWorkspaceId,
|
||||
} = block.props;
|
||||
const blockId = component.service.crud.addBlock(
|
||||
AIChatBlockFlavour,
|
||||
{
|
||||
xywh,
|
||||
scale,
|
||||
messages,
|
||||
sessionId,
|
||||
rootDocId,
|
||||
rootWorkspaceId,
|
||||
},
|
||||
component.surface.model.id
|
||||
);
|
||||
return blockId;
|
||||
};
|
||||
component.clipboardController.registerBlock(
|
||||
AIChatBlockFlavour,
|
||||
createFunc
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return EdgelessClipboardWatcher;
|
||||
}
|
||||
|
||||
@customElement('affine-linked-doc-ref-block')
|
||||
export class LinkedDocBlockComponent extends EmbedLinkedDocBlockComponent {
|
||||
override getInitialState() {
|
||||
return {
|
||||
loading: false,
|
||||
isBannerEmpty: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function patchForSharedPage() {
|
||||
const extension: ExtensionType = {
|
||||
setup: di => {
|
||||
di.override(
|
||||
BlockViewIdentifier('affine:embed-linked-doc'),
|
||||
() => literal`affine-linked-doc-ref-block`
|
||||
);
|
||||
di.override(
|
||||
BlockViewIdentifier('affine:embed-synced-doc'),
|
||||
() => literal`affine-linked-doc-ref-block`
|
||||
);
|
||||
},
|
||||
};
|
||||
return extension;
|
||||
}
|
||||
|
||||
export function patchForMobile() {
|
||||
class MobileSpecsPatches extends LifeCycleWatcher {
|
||||
static override key = 'mobile-patches';
|
||||
|
||||
constructor(std: BlockStdScope) {
|
||||
super(std);
|
||||
const featureFlagService = std.get(FeatureFlagService);
|
||||
|
||||
featureFlagService.setFlag('enable_mobile_keyboard_toolbar', true);
|
||||
featureFlagService.setFlag('enable_mobile_linked_doc_menu', true);
|
||||
}
|
||||
|
||||
static override setup(di: Container) {
|
||||
super.setup(di);
|
||||
|
||||
// Hide reference popup on mobile.
|
||||
{
|
||||
const prev = di.getFactory(ReferenceNodeConfigIdentifier);
|
||||
di.override(ReferenceNodeConfigIdentifier, provider => {
|
||||
return {
|
||||
...prev?.(provider),
|
||||
hidePopup: true,
|
||||
} satisfies ReferenceNodeConfig;
|
||||
});
|
||||
}
|
||||
|
||||
// Hide number lines for code block on mobile.
|
||||
{
|
||||
const codeConfigIdentifier = ConfigIdentifier('affine:code');
|
||||
const prev = di.getFactory(codeConfigIdentifier);
|
||||
di.override(codeConfigIdentifier, provider => {
|
||||
return {
|
||||
...prev?.(provider),
|
||||
showLineNumbers: false,
|
||||
} satisfies CodeBlockConfig;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
override mounted() {
|
||||
// remove slash placeholder for mobile: `type / ...`
|
||||
{
|
||||
const paragraphService = this.std.get(ParagraphBlockService);
|
||||
if (!paragraphService) return;
|
||||
|
||||
paragraphService.placeholderGenerator = model => {
|
||||
const placeholders = {
|
||||
text: '',
|
||||
h1: 'Heading 1',
|
||||
h2: 'Heading 2',
|
||||
h3: 'Heading 3',
|
||||
h4: 'Heading 4',
|
||||
h5: 'Heading 5',
|
||||
h6: 'Heading 6',
|
||||
quote: '',
|
||||
};
|
||||
return placeholders[model.type];
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
const extensions: ExtensionType[] = [
|
||||
{
|
||||
setup: di => {
|
||||
const prev = di.getFactory(RootBlockConfigExtension.identifier);
|
||||
|
||||
di.override(RootBlockConfigExtension.identifier, provider => {
|
||||
return {
|
||||
...prev?.(provider),
|
||||
keyboardToolbar: createKeyboardToolbarConfig(),
|
||||
} satisfies RootBlockConfig;
|
||||
});
|
||||
},
|
||||
},
|
||||
MobileSpecsPatches,
|
||||
];
|
||||
return extensions;
|
||||
}
|
||||
|
||||
export function patchForAttachmentEmbedViews(
|
||||
reactToLit: (
|
||||
element: ElementOrFactory,
|
||||
rerendering?: boolean
|
||||
) => TemplateResult
|
||||
): ExtensionType {
|
||||
return {
|
||||
setup: di => {
|
||||
di.override(AttachmentEmbedConfigIdentifier('pdf'), () => ({
|
||||
name: 'pdf',
|
||||
check: (model, maxFileSize) =>
|
||||
model.type === 'application/pdf' && model.size <= maxFileSize,
|
||||
action: model => {
|
||||
const bound = Bound.deserialize(model.xywh);
|
||||
bound.w = 537 + 24 + 2;
|
||||
bound.h = 759 + 46 + 24 + 2;
|
||||
model.doc.updateBlock(model, {
|
||||
embed: true,
|
||||
style: 'pdf',
|
||||
xywh: bound.serialize(),
|
||||
});
|
||||
},
|
||||
template: (model, _blobUrl) =>
|
||||
reactToLit(<AttachmentEmbedPreview model={model} />, false),
|
||||
}));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function patchForClipboardInElectron(framework: FrameworkProvider) {
|
||||
const desktopApi = framework.get(DesktopApiService);
|
||||
return NativeClipboardExtension({
|
||||
copyAsPNG: desktopApi.handler.clipboard.copyAsPNG,
|
||||
});
|
||||
}
|
||||
|
||||
export function patchForEdgelessNoteConfig(
|
||||
framework: FrameworkProvider,
|
||||
reactToLit: (element: ElementOrFactory) => TemplateResult
|
||||
) {
|
||||
return NoteConfigExtension({
|
||||
edgelessNoteHeader: ({ note }) =>
|
||||
reactToLit(<EdgelessNoteHeader note={note} />),
|
||||
pageBlockTitle: ({ note }) => {
|
||||
const journalService = framework.get(JournalService);
|
||||
const isJournal = !!journalService.journalDate$(note.doc.id).value;
|
||||
if (isJournal) {
|
||||
return reactToLit(<BlocksuiteEditorJournalDocTitle page={note.doc} />);
|
||||
} else {
|
||||
return html`<doc-title .doc=${note.doc}></doc-title>`;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function patchSideBarService(framework: FrameworkProvider) {
|
||||
const { workbench } = framework.get(WorkbenchService);
|
||||
|
||||
return SidebarExtension({
|
||||
open: (tabId?: string) => {
|
||||
workbench.openSidebar();
|
||||
workbench.activeView$.value.activeSidebarTab(tabId ?? null);
|
||||
},
|
||||
close: () => {
|
||||
workbench.closeSidebar();
|
||||
},
|
||||
getTabIds: () => {
|
||||
return workbench.activeView$.value.sidebarTabs$.value.map(tab => tab.id);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,23 +1,17 @@
|
||||
import { enableAIExtension } from '@affine/core/blocksuite/ai';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { enableAffineExtension } from '@affine/core/blocksuite/extensions';
|
||||
import { builtInTemplates as builtInEdgelessTemplates } from '@affine/templates/edgeless';
|
||||
import { builtInTemplates as builtInStickersTemplates } from '@affine/templates/stickers';
|
||||
import type { SpecBuilder, TemplateManager } from '@blocksuite/affine/blocks';
|
||||
import { EdgelessTemplatePanel, SpecProvider } from '@blocksuite/affine/blocks';
|
||||
import { type FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
import { enableAffineExtension } from './custom/root-block';
|
||||
|
||||
export function createEdgelessModeSpecs(
|
||||
framework: FrameworkProvider
|
||||
): SpecBuilder {
|
||||
const featureFlagService = framework.get(FeatureFlagService);
|
||||
const enableAI = featureFlagService.flags.enable_ai.value;
|
||||
const edgelessSpec = SpecProvider._.getSpec('edgeless');
|
||||
enableAffineExtension(framework, edgelessSpec);
|
||||
if (enableAI) {
|
||||
enableAIExtension(edgelessSpec, framework);
|
||||
}
|
||||
enableAffineExtension(edgelessSpec, framework);
|
||||
enableAIExtension(edgelessSpec, framework);
|
||||
|
||||
return edgelessSpec;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
import { enableAIExtension } from '@affine/core/blocksuite/ai';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { enableAffineExtension } from '@affine/core/blocksuite/extensions';
|
||||
import { type SpecBuilder, SpecProvider } from '@blocksuite/affine/blocks';
|
||||
import { type FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
import { enableAffineExtension } from './custom/root-block';
|
||||
|
||||
export function createPageModeSpecs(framework: FrameworkProvider): SpecBuilder {
|
||||
const featureFlagService = framework.get(FeatureFlagService);
|
||||
const enableAI = featureFlagService.flags.enable_ai.value;
|
||||
const provider = SpecProvider._;
|
||||
const pageSpec = provider.getSpec('page');
|
||||
enableAffineExtension(framework, pageSpec);
|
||||
if (enableAI) {
|
||||
enableAIExtension(pageSpec, framework);
|
||||
}
|
||||
const pageSpec = SpecProvider._.getSpec('page');
|
||||
enableAffineExtension(pageSpec, framework);
|
||||
enableAIExtension(pageSpec, framework);
|
||||
return pageSpec;
|
||||
}
|
||||
|
||||
@@ -20,9 +20,10 @@ import type { ExtensionType } from '@blocksuite/affine/store';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
import type { Observable } from 'rxjs';
|
||||
|
||||
import { buildDocDisplayMetaExtension } from './custom/root-block';
|
||||
import { patchPeekViewService } from './custom/spec-patchers';
|
||||
import { getFontConfigExtension } from './font-extension';
|
||||
import { buildDocDisplayMetaExtension } from '../../extensions/display-meta';
|
||||
import { getFontConfigExtension } from '../../extensions/font-config';
|
||||
import { patchPeekViewService } from '../../extensions/peek-view-service';
|
||||
import { getThemeExtension } from '../../extensions/theme';
|
||||
|
||||
const CustomSpecs: ExtensionType[] = [
|
||||
AIChatBlockSpec,
|
||||
@@ -111,3 +112,19 @@ export function createPageModePreviewSpecs(
|
||||
]);
|
||||
return pagePreviewSpec;
|
||||
}
|
||||
|
||||
export const extendEdgelessPreviewSpec = (function () {
|
||||
let _extension: ExtensionType;
|
||||
let _framework: FrameworkProvider;
|
||||
return function (framework: FrameworkProvider) {
|
||||
if (framework === _framework && _extension) {
|
||||
return _extension;
|
||||
} else {
|
||||
_extension && SpecProvider._.omitSpec('preview:edgeless', _extension);
|
||||
_extension = getThemeExtension(framework);
|
||||
_framework = framework;
|
||||
SpecProvider._.extendSpec('preview:edgeless', [_extension]);
|
||||
return _extension;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
import { createReactComponentFromLit } from '@affine/component';
|
||||
import { DocTitle } from '@blocksuite/affine/blocks';
|
||||
import React from 'react';
|
||||
|
||||
import { EdgelessEditor } from './edgeless-editor';
|
||||
import { PageEditor } from './page-editor';
|
||||
|
||||
export * from './edgeless-editor';
|
||||
export * from './page-editor';
|
||||
|
||||
export const LitDocEditor = createReactComponentFromLit({
|
||||
react: React,
|
||||
elementClass: PageEditor,
|
||||
});
|
||||
|
||||
export const LitDocTitle = createReactComponentFromLit({
|
||||
react: React,
|
||||
elementClass: DocTitle,
|
||||
});
|
||||
|
||||
export const LitEdgelessEditor = createReactComponentFromLit({
|
||||
react: React,
|
||||
elementClass: EdgelessEditor,
|
||||
});
|
||||
|
||||
export function effects() {
|
||||
customElements.define('page-editor', PageEditor);
|
||||
customElements.define('edgeless-editor', EdgelessEditor);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { ElementOrFactory } from '@affine/component';
|
||||
import { AttachmentEmbedPreview } from '@affine/core/components/attachment-viewer/pdf-viewer-embedded';
|
||||
import { AttachmentEmbedConfigIdentifier } from '@blocksuite/affine/blocks';
|
||||
import { Bound } from '@blocksuite/affine/global/utils';
|
||||
import type { ExtensionType } from '@blocksuite/affine/store';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
export function patchForAttachmentEmbedViews(
|
||||
reactToLit: (
|
||||
element: ElementOrFactory,
|
||||
rerendering?: boolean
|
||||
) => TemplateResult
|
||||
): ExtensionType {
|
||||
return {
|
||||
setup: di => {
|
||||
di.override(AttachmentEmbedConfigIdentifier('pdf'), () => ({
|
||||
name: 'pdf',
|
||||
check: (model, maxFileSize) =>
|
||||
model.type === 'application/pdf' && model.size <= maxFileSize,
|
||||
action: model => {
|
||||
const bound = Bound.deserialize(model.xywh);
|
||||
bound.w = 537 + 24 + 2;
|
||||
bound.h = 759 + 46 + 24 + 2;
|
||||
model.doc.updateBlock(model, {
|
||||
embed: true,
|
||||
style: 'pdf',
|
||||
xywh: bound.serialize(),
|
||||
});
|
||||
},
|
||||
template: (model, _blobUrl) =>
|
||||
reactToLit(<AttachmentEmbedPreview model={model} />, false),
|
||||
}));
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||
import { LifeCycleWatcher, StdIdentifier } from '@blocksuite/affine/block-std';
|
||||
import type {
|
||||
DocDisplayMetaExtension,
|
||||
DocDisplayMetaParams,
|
||||
Signal,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import {
|
||||
createSignalFromObservable,
|
||||
DocDisplayMetaProvider,
|
||||
referenceToNode,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import type { Container } from '@blocksuite/affine/global/di';
|
||||
import { LinkedPageIcon, PageIcon } from '@blocksuite/icons/lit';
|
||||
import { type FrameworkProvider } from '@toeverything/infra';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
export function buildDocDisplayMetaExtension(framework: FrameworkProvider) {
|
||||
const docDisplayMetaService = framework.get(DocDisplayMetaService);
|
||||
|
||||
function iconBuilder(
|
||||
icon: typeof PageIcon,
|
||||
size = '1.25em',
|
||||
style = 'user-select:none;flex-shrink:0;vertical-align:middle;font-size:inherit;margin-bottom:0.1em;'
|
||||
) {
|
||||
return icon({
|
||||
width: size,
|
||||
height: size,
|
||||
style,
|
||||
});
|
||||
}
|
||||
|
||||
class AffineDocDisplayMetaService
|
||||
extends LifeCycleWatcher
|
||||
implements DocDisplayMetaExtension
|
||||
{
|
||||
static override key = 'doc-display-meta';
|
||||
|
||||
readonly disposables: (() => void)[] = [];
|
||||
|
||||
static override setup(di: Container) {
|
||||
super.setup(di);
|
||||
di.override(DocDisplayMetaProvider, this, [StdIdentifier]);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
while (this.disposables.length > 0) {
|
||||
this.disposables.pop()?.();
|
||||
}
|
||||
}
|
||||
|
||||
icon(
|
||||
docId: string,
|
||||
{ params, title, referenced }: DocDisplayMetaParams = {}
|
||||
): Signal<TemplateResult> {
|
||||
const icon$ = docDisplayMetaService
|
||||
.icon$(docId, {
|
||||
type: 'lit',
|
||||
title,
|
||||
reference: referenced,
|
||||
referenceToNode: referenceToNode({ pageId: docId, params }),
|
||||
})
|
||||
.map(iconBuilder);
|
||||
|
||||
const { signal: iconSignal, cleanup } = createSignalFromObservable(
|
||||
icon$,
|
||||
iconBuilder(referenced ? LinkedPageIcon : PageIcon)
|
||||
);
|
||||
|
||||
this.disposables.push(cleanup);
|
||||
|
||||
return iconSignal;
|
||||
}
|
||||
|
||||
title(
|
||||
docId: string,
|
||||
{ title, referenced }: DocDisplayMetaParams = {}
|
||||
): Signal<string> {
|
||||
const title$ = docDisplayMetaService.title$(docId, {
|
||||
title,
|
||||
reference: referenced,
|
||||
});
|
||||
|
||||
const { signal: titleSignal, cleanup } =
|
||||
createSignalFromObservable<string>(title$, title ?? '');
|
||||
|
||||
this.disposables.push(cleanup);
|
||||
|
||||
return titleSignal;
|
||||
}
|
||||
|
||||
override unmounted() {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
return AffineDocDisplayMetaService;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { DocService, DocsService } from '@affine/core/modules/doc';
|
||||
import type { EditorService } from '@affine/core/modules/editor';
|
||||
import type { DocMode, DocModeProvider } from '@blocksuite/affine/blocks';
|
||||
import { DocModeExtension } from '@blocksuite/affine/blocks';
|
||||
import type { ExtensionType } from '@blocksuite/affine/store';
|
||||
|
||||
export function patchDocModeService(
|
||||
docService: DocService,
|
||||
docsService: DocsService,
|
||||
editorService: EditorService
|
||||
): ExtensionType {
|
||||
const DEFAULT_MODE = 'page';
|
||||
class AffineDocModeService implements DocModeProvider {
|
||||
setEditorMode = (mode: DocMode) => {
|
||||
editorService.editor.setMode(mode);
|
||||
};
|
||||
getEditorMode = () => {
|
||||
return editorService.editor.mode$.value;
|
||||
};
|
||||
setPrimaryMode = (mode: DocMode, id?: string) => {
|
||||
if (id) {
|
||||
docsService.list.setPrimaryMode(id, mode);
|
||||
} else {
|
||||
docService.doc.setPrimaryMode(mode);
|
||||
}
|
||||
};
|
||||
getPrimaryMode = (id?: string) => {
|
||||
const mode = id
|
||||
? docsService.list.getPrimaryMode(id)
|
||||
: docService.doc.getPrimaryMode();
|
||||
return (mode || DEFAULT_MODE) as DocMode;
|
||||
};
|
||||
togglePrimaryMode = (id?: string) => {
|
||||
const mode = id
|
||||
? docsService.list.togglePrimaryMode(id)
|
||||
: docService.doc.togglePrimaryMode();
|
||||
return (mode || DEFAULT_MODE) as DocMode;
|
||||
};
|
||||
onPrimaryModeChange = (handler: (mode: DocMode) => void, id?: string) => {
|
||||
const mode$ = id
|
||||
? docsService.list.primaryMode$(id)
|
||||
: docService.doc.primaryMode$;
|
||||
const sub = mode$.subscribe(m => handler((m || DEFAULT_MODE) as DocMode));
|
||||
return {
|
||||
dispose: sub.unsubscribe,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const docModeExtension = DocModeExtension(new AffineDocModeService());
|
||||
|
||||
return docModeExtension;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { generateUrl } from '@affine/core/components/hooks/affine/use-share-url';
|
||||
import { WorkspaceServerService } from '@affine/core/modules/cloud';
|
||||
import { resolveLinkToDoc } from '@affine/core/modules/navigation/utils';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import {
|
||||
GenerateDocUrlExtension,
|
||||
ParseDocUrlExtension,
|
||||
type ReferenceParams,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
function patchParseDocUrlExtension(framework: FrameworkProvider) {
|
||||
const workspaceService = framework.get(WorkspaceService);
|
||||
const ParseDocUrl = ParseDocUrlExtension({
|
||||
parseDocUrl(url) {
|
||||
const info = resolveLinkToDoc(url);
|
||||
if (!info || info.workspaceId !== workspaceService.workspace.id) return;
|
||||
|
||||
delete info.refreshKey;
|
||||
|
||||
return info;
|
||||
},
|
||||
});
|
||||
|
||||
return ParseDocUrl;
|
||||
}
|
||||
|
||||
function patchGenerateDocUrlExtension(framework: FrameworkProvider) {
|
||||
const workspaceService = framework.get(WorkspaceService);
|
||||
const workspaceServerService = framework.get(WorkspaceServerService);
|
||||
const GenerateDocUrl = GenerateDocUrlExtension({
|
||||
generateDocUrl(pageId: string, params?: ReferenceParams) {
|
||||
return generateUrl({
|
||||
...params,
|
||||
pageId,
|
||||
workspaceId: workspaceService.workspace.id,
|
||||
baseUrl: workspaceServerService.server?.baseUrl ?? location.origin,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return GenerateDocUrl;
|
||||
}
|
||||
|
||||
export function patchDocUrlExtensions(framework: FrameworkProvider) {
|
||||
return [
|
||||
patchParseDocUrlExtension(framework),
|
||||
patchGenerateDocUrlExtension(framework),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { AIChatBlockSchema } from '@affine/core/blocksuite/ai/blocks';
|
||||
import { BlockServiceWatcher } from '@blocksuite/affine/block-std';
|
||||
import { EdgelessRootBlockComponent } from '@blocksuite/affine/blocks';
|
||||
import type { BlockSnapshot } from '@blocksuite/affine/store';
|
||||
|
||||
export class EdgelessClipboardWatcher extends BlockServiceWatcher {
|
||||
static override readonly flavour = 'affine:page';
|
||||
|
||||
override mounted() {
|
||||
super.mounted();
|
||||
this.blockService.disposables.add(
|
||||
this.blockService.specSlots.viewConnected.on(view => {
|
||||
const { component } = view;
|
||||
if (component instanceof EdgelessRootBlockComponent) {
|
||||
const AIChatBlockFlavour = AIChatBlockSchema.model.flavour;
|
||||
const createFunc = (block: BlockSnapshot) => {
|
||||
const {
|
||||
xywh,
|
||||
scale,
|
||||
messages,
|
||||
sessionId,
|
||||
rootDocId,
|
||||
rootWorkspaceId,
|
||||
} = block.props;
|
||||
const blockId = component.service.crud.addBlock(
|
||||
AIChatBlockFlavour,
|
||||
{
|
||||
xywh,
|
||||
scale,
|
||||
messages,
|
||||
sessionId,
|
||||
rootDocId,
|
||||
rootWorkspaceId,
|
||||
},
|
||||
component.surface.model.id
|
||||
);
|
||||
return blockId;
|
||||
};
|
||||
component.clipboardController.registerBlock(
|
||||
AIChatBlockFlavour,
|
||||
createFunc
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import {
|
||||
DatabaseConfigExtension,
|
||||
EditorSettingExtension,
|
||||
RootBlockConfigExtension,
|
||||
ToolbarMoreMenuConfigExtension,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import type { ExtensionType } from '@blocksuite/affine/store';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
import { createDatabaseOptionsConfig } from './database';
|
||||
import { createLinkedWidgetConfig } from './linked';
|
||||
import { createToolbarMoreMenuConfig } from './toolbar';
|
||||
|
||||
export function getEditorConfigExtension(
|
||||
framework: FrameworkProvider
|
||||
): ExtensionType[] {
|
||||
const editorSettingService = framework.get(EditorSettingService);
|
||||
return [
|
||||
EditorSettingExtension(editorSettingService.editorSetting.settingSignal),
|
||||
DatabaseConfigExtension(createDatabaseOptionsConfig(framework)),
|
||||
RootBlockConfigExtension({
|
||||
linkedWidget: createLinkedWidgetConfig(framework),
|
||||
}),
|
||||
ToolbarMoreMenuConfigExtension(createToolbarMoreMenuConfig(framework)),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { DesktopApiService } from '@affine/core/modules/desktop-api';
|
||||
import { NativeClipboardExtension } from '@blocksuite/affine/blocks';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
export function patchForClipboardInElectron(framework: FrameworkProvider) {
|
||||
const desktopApi = framework.get(DesktopApiService);
|
||||
return NativeClipboardExtension({
|
||||
copyAsPNG: desktopApi.handler.clipboard.copyAsPNG,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { SpecBuilder } from '@blocksuite/affine/blocks';
|
||||
import { type FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
import { buildDocDisplayMetaExtension } from '../display-meta';
|
||||
import { getEditorConfigExtension } from '../editor-config';
|
||||
import { getFontConfigExtension } from '../font-config';
|
||||
import { getTelemetryExtension } from '../telemetry';
|
||||
import { getThemeExtension } from '../theme';
|
||||
|
||||
export function enableAffineExtension(
|
||||
specBuilder: SpecBuilder,
|
||||
framework: FrameworkProvider
|
||||
): void {
|
||||
specBuilder.extend(
|
||||
[
|
||||
getThemeExtension(framework),
|
||||
getFontConfigExtension(),
|
||||
getTelemetryExtension(),
|
||||
getEditorConfigExtension(framework),
|
||||
buildDocDisplayMetaExtension(framework),
|
||||
].flat()
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { createKeyboardToolbarConfig } from '@affine/core/blocksuite/extensions/keyboard-toolbar-config';
|
||||
import {
|
||||
type BlockStdScope,
|
||||
ConfigIdentifier,
|
||||
LifeCycleWatcher,
|
||||
} from '@blocksuite/affine/block-std';
|
||||
import type {
|
||||
CodeBlockConfig,
|
||||
ReferenceNodeConfig,
|
||||
RootBlockConfig,
|
||||
SpecBuilder,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import {
|
||||
codeToolbarWidget,
|
||||
embedCardToolbarWidget,
|
||||
FeatureFlagService,
|
||||
formatBarWidget,
|
||||
imageToolbarWidget,
|
||||
ParagraphBlockService,
|
||||
ReferenceNodeConfigIdentifier,
|
||||
RootBlockConfigExtension,
|
||||
slashMenuWidget,
|
||||
surfaceRefToolbarWidget,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import type { Container } from '@blocksuite/affine/global/di';
|
||||
import type { ExtensionType } from '@blocksuite/affine/store';
|
||||
|
||||
class MobileSpecsPatches extends LifeCycleWatcher {
|
||||
static override key = 'mobile-patches';
|
||||
|
||||
constructor(std: BlockStdScope) {
|
||||
super(std);
|
||||
const featureFlagService = std.get(FeatureFlagService);
|
||||
|
||||
featureFlagService.setFlag('enable_mobile_keyboard_toolbar', true);
|
||||
featureFlagService.setFlag('enable_mobile_linked_doc_menu', true);
|
||||
}
|
||||
|
||||
static override setup(di: Container) {
|
||||
super.setup(di);
|
||||
|
||||
// Hide reference popup on mobile.
|
||||
{
|
||||
const prev = di.getFactory(ReferenceNodeConfigIdentifier);
|
||||
di.override(ReferenceNodeConfigIdentifier, provider => {
|
||||
return {
|
||||
...prev?.(provider),
|
||||
hidePopup: true,
|
||||
} satisfies ReferenceNodeConfig;
|
||||
});
|
||||
}
|
||||
|
||||
// Hide number lines for code block on mobile.
|
||||
{
|
||||
const codeConfigIdentifier = ConfigIdentifier('affine:code');
|
||||
const prev = di.getFactory(codeConfigIdentifier);
|
||||
di.override(codeConfigIdentifier, provider => {
|
||||
return {
|
||||
...prev?.(provider),
|
||||
showLineNumbers: false,
|
||||
} satisfies CodeBlockConfig;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
override mounted() {
|
||||
// remove slash placeholder for mobile: `type / ...`
|
||||
{
|
||||
const paragraphService = this.std.get(ParagraphBlockService);
|
||||
if (!paragraphService) return;
|
||||
|
||||
paragraphService.placeholderGenerator = model => {
|
||||
const placeholders = {
|
||||
text: '',
|
||||
h1: 'Heading 1',
|
||||
h2: 'Heading 2',
|
||||
h3: 'Heading 3',
|
||||
h4: 'Heading 4',
|
||||
h5: 'Heading 5',
|
||||
h6: 'Heading 6',
|
||||
quote: '',
|
||||
};
|
||||
return placeholders[model.type];
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mobileExtensions: ExtensionType[] = [
|
||||
{
|
||||
setup: di => {
|
||||
const prev = di.getFactory(RootBlockConfigExtension.identifier);
|
||||
|
||||
di.override(RootBlockConfigExtension.identifier, provider => {
|
||||
return {
|
||||
...prev?.(provider),
|
||||
keyboardToolbar: createKeyboardToolbarConfig(),
|
||||
} satisfies RootBlockConfig;
|
||||
});
|
||||
},
|
||||
},
|
||||
MobileSpecsPatches,
|
||||
];
|
||||
|
||||
export function enableMobileExtension(specBuilder: SpecBuilder): void {
|
||||
specBuilder.omit(formatBarWidget);
|
||||
specBuilder.omit(embedCardToolbarWidget);
|
||||
specBuilder.omit(slashMenuWidget);
|
||||
specBuilder.omit(codeToolbarWidget);
|
||||
specBuilder.omit(imageToolbarWidget);
|
||||
specBuilder.omit(surfaceRefToolbarWidget);
|
||||
specBuilder.extend(mobileExtensions);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './entry/enable-affine';
|
||||
export * from './entry/enable-mobile';
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { ElementOrFactory } from '@affine/component';
|
||||
import { JournalService } from '@affine/core/modules/journal';
|
||||
import { NoteConfigExtension } from '@blocksuite/affine/blocks';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
import { html, type TemplateResult } from 'lit';
|
||||
|
||||
import { BlocksuiteEditorJournalDocTitle } from '../../block-suite-editor/journal-doc-title';
|
||||
import { EdgelessNoteHeader } from './edgeless-note-header';
|
||||
|
||||
export function patchForEdgelessNoteConfig(
|
||||
framework: FrameworkProvider,
|
||||
reactToLit: (element: ElementOrFactory) => TemplateResult
|
||||
) {
|
||||
return NoteConfigExtension({
|
||||
edgelessNoteHeader: ({ note }) =>
|
||||
reactToLit(<EdgelessNoteHeader note={note} />),
|
||||
pageBlockTitle: ({ note }) => {
|
||||
const journalService = framework.get(JournalService);
|
||||
const isJournal = !!journalService.journalDate$(note.doc.id).value;
|
||||
if (isJournal) {
|
||||
return reactToLit(<BlocksuiteEditorJournalDocTitle page={note.doc} />);
|
||||
} else {
|
||||
return html`<doc-title .doc=${note.doc}></doc-title>`;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import {
|
||||
Input,
|
||||
notify,
|
||||
toast,
|
||||
type ToastOptions,
|
||||
toReactNode,
|
||||
type useConfirmModal,
|
||||
} from '@affine/component';
|
||||
import { NotificationExtension } from '@blocksuite/affine/blocks';
|
||||
|
||||
export function patchNotificationService({
|
||||
closeConfirmModal,
|
||||
openConfirmModal,
|
||||
}: ReturnType<typeof useConfirmModal>) {
|
||||
return NotificationExtension({
|
||||
confirm: async ({ title, message, confirmText, cancelText, abort }) => {
|
||||
return new Promise<boolean>(resolve => {
|
||||
openConfirmModal({
|
||||
title: toReactNode(title),
|
||||
description: toReactNode(message),
|
||||
confirmText,
|
||||
confirmButtonOptions: {
|
||||
variant: 'primary',
|
||||
},
|
||||
cancelText,
|
||||
onConfirm: () => {
|
||||
resolve(true);
|
||||
},
|
||||
onCancel: () => {
|
||||
resolve(false);
|
||||
},
|
||||
});
|
||||
abort?.addEventListener('abort', () => {
|
||||
resolve(false);
|
||||
closeConfirmModal();
|
||||
});
|
||||
});
|
||||
},
|
||||
prompt: async ({
|
||||
title,
|
||||
message,
|
||||
confirmText,
|
||||
placeholder,
|
||||
cancelText,
|
||||
autofill,
|
||||
abort,
|
||||
}) => {
|
||||
return new Promise<string | null>(resolve => {
|
||||
let value = autofill || '';
|
||||
const description = (
|
||||
<div>
|
||||
<span style={{ marginBottom: 12 }}>{toReactNode(message)}</span>
|
||||
<Input
|
||||
autoSelect={true}
|
||||
placeholder={placeholder}
|
||||
defaultValue={value}
|
||||
onChange={e => (value = e)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
openConfirmModal({
|
||||
title: toReactNode(title),
|
||||
description: description,
|
||||
confirmText: confirmText ?? 'Confirm',
|
||||
confirmButtonOptions: {
|
||||
variant: 'primary',
|
||||
},
|
||||
cancelText: cancelText ?? 'Cancel',
|
||||
onConfirm: () => {
|
||||
resolve(value);
|
||||
},
|
||||
onCancel: () => {
|
||||
resolve(null);
|
||||
},
|
||||
autoFocusConfirm: false,
|
||||
});
|
||||
abort?.addEventListener('abort', () => {
|
||||
resolve(null);
|
||||
closeConfirmModal();
|
||||
});
|
||||
});
|
||||
},
|
||||
toast: (message: string, options: ToastOptions) => {
|
||||
return toast(message, options);
|
||||
},
|
||||
notify: notification => {
|
||||
const accentToNotify = {
|
||||
error: notify.error,
|
||||
success: notify.success,
|
||||
warning: notify.warning,
|
||||
info: notify,
|
||||
};
|
||||
|
||||
const fn = accentToNotify[notification.accent || 'info'];
|
||||
if (!fn) {
|
||||
throw new Error('Invalid notification accent');
|
||||
}
|
||||
|
||||
const toastId = fn(
|
||||
{
|
||||
title: toReactNode(notification.title),
|
||||
message: toReactNode(notification.message),
|
||||
footer: toReactNode(notification.footer),
|
||||
action: notification.action?.onClick
|
||||
? {
|
||||
label: toReactNode(notification.action?.label),
|
||||
onClick: notification.action.onClick,
|
||||
}
|
||||
: undefined,
|
||||
onDismiss: notification.onClose,
|
||||
},
|
||||
{
|
||||
duration: notification.duration || 0,
|
||||
onDismiss: notification.onClose,
|
||||
onAutoClose: notification.onClose,
|
||||
}
|
||||
);
|
||||
|
||||
notification.abort?.addEventListener('abort', () => {
|
||||
notify.dismiss(toastId);
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { I18n } from '@affine/i18n';
|
||||
import type {
|
||||
OpenDocConfig,
|
||||
OpenDocConfigItem,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import { OpenDocExtension } from '@blocksuite/affine/blocks';
|
||||
import {
|
||||
CenterPeekIcon,
|
||||
ExpandFullIcon,
|
||||
OpenInNewIcon,
|
||||
SplitViewIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
|
||||
export function patchOpenDocExtension() {
|
||||
const openDocConfig: OpenDocConfig = {
|
||||
items: [
|
||||
{
|
||||
type: 'open-in-active-view',
|
||||
label: I18n['com.affine.peek-view-controls.open-doc'](),
|
||||
icon: ExpandFullIcon(),
|
||||
},
|
||||
BUILD_CONFIG.isElectron
|
||||
? {
|
||||
type: 'open-in-new-view',
|
||||
label:
|
||||
I18n['com.affine.peek-view-controls.open-doc-in-split-view'](),
|
||||
icon: SplitViewIcon(),
|
||||
}
|
||||
: null,
|
||||
{
|
||||
type: 'open-in-new-tab',
|
||||
label: I18n['com.affine.peek-view-controls.open-doc-in-new-tab'](),
|
||||
icon: OpenInNewIcon(),
|
||||
},
|
||||
{
|
||||
type: 'open-in-center-peek',
|
||||
label: I18n['com.affine.peek-view-controls.open-doc-in-center-peek'](),
|
||||
icon: CenterPeekIcon(),
|
||||
},
|
||||
].filter((item): item is OpenDocConfigItem => item !== null),
|
||||
};
|
||||
return OpenDocExtension(openDocConfig);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { PeekViewService } from '@affine/core/modules/peek-view';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import type {
|
||||
PeekOptions,
|
||||
PeekViewService as BSPeekViewService,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import { PeekViewExtension } from '@blocksuite/affine/blocks';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
const logger = new DebugLogger('affine::patch-peek-view-service');
|
||||
|
||||
export function patchPeekViewService(service: PeekViewService) {
|
||||
return PeekViewExtension({
|
||||
peek: (
|
||||
element: {
|
||||
target: HTMLElement;
|
||||
docId: string;
|
||||
blockIds?: string[];
|
||||
template?: TemplateResult;
|
||||
},
|
||||
options?: PeekOptions
|
||||
) => {
|
||||
logger.debug('center peek', element);
|
||||
const { template, target, ...props } = element;
|
||||
|
||||
return service.peekView.open(
|
||||
{
|
||||
element: target,
|
||||
docRef: props,
|
||||
},
|
||||
template,
|
||||
options?.abortSignal
|
||||
);
|
||||
},
|
||||
} satisfies BSPeekViewService);
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import { DocsService } from '@affine/core/modules/doc';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import {
|
||||
CreationQuickSearchSession,
|
||||
DocsQuickSearchSession,
|
||||
LinksQuickSearchSession,
|
||||
QuickSearchService,
|
||||
RecentDocsQuickSearchSession,
|
||||
} from '@affine/core/modules/quicksearch';
|
||||
import { ExternalLinksQuickSearchSession } from '@affine/core/modules/quicksearch/impls/external-links';
|
||||
import { JournalsQuickSearchSession } from '@affine/core/modules/quicksearch/impls/journals';
|
||||
import { track } from '@affine/track';
|
||||
import {
|
||||
BlockServiceWatcher,
|
||||
type WidgetComponent,
|
||||
} from '@blocksuite/affine/block-std';
|
||||
import type { QuickSearchResult } from '@blocksuite/affine/blocks';
|
||||
import {
|
||||
AffineSlashMenuWidget,
|
||||
insertLinkByQuickSearchCommand,
|
||||
QuickSearchExtension,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import { Text } from '@blocksuite/affine/store';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
import { pick } from 'lodash-es';
|
||||
|
||||
import type { DocProps } from '../initialization';
|
||||
|
||||
export function patchQuickSearchService(framework: FrameworkProvider) {
|
||||
const QuickSearch = QuickSearchExtension({
|
||||
async openQuickSearch() {
|
||||
let searchResult: QuickSearchResult = null;
|
||||
searchResult = await new Promise((resolve, reject) =>
|
||||
framework.get(QuickSearchService).quickSearch.show(
|
||||
[
|
||||
framework.get(RecentDocsQuickSearchSession),
|
||||
framework.get(CreationQuickSearchSession),
|
||||
framework.get(DocsQuickSearchSession),
|
||||
framework.get(LinksQuickSearchSession),
|
||||
framework.get(ExternalLinksQuickSearchSession),
|
||||
framework.get(JournalsQuickSearchSession),
|
||||
],
|
||||
result => {
|
||||
if (result === null) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.source === 'docs') {
|
||||
resolve({
|
||||
docId: result.payload.docId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.source === 'recent-doc') {
|
||||
resolve({
|
||||
docId: result.payload.docId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.source === 'link') {
|
||||
resolve({
|
||||
docId: result.payload.docId,
|
||||
params: pick(result.payload, [
|
||||
'mode',
|
||||
'blockIds',
|
||||
'elementIds',
|
||||
]),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.source === 'date-picker') {
|
||||
result.payload
|
||||
.getDocId()
|
||||
.then(docId => {
|
||||
if (docId) {
|
||||
resolve({ docId });
|
||||
}
|
||||
})
|
||||
.catch(reject);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.source === 'external-link') {
|
||||
const externalUrl = result.payload.url;
|
||||
resolve({ externalUrl });
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.source === 'creation') {
|
||||
const docsService = framework.get(DocsService);
|
||||
const editorSettingService = framework.get(EditorSettingService);
|
||||
const mode =
|
||||
result.id === 'creation:create-edgeless' ? 'edgeless' : 'page';
|
||||
const docProps: DocProps = {
|
||||
page: { title: new Text(result.payload.title) },
|
||||
note: editorSettingService.editorSetting.get('affine:note'),
|
||||
};
|
||||
const newDoc = docsService.createDoc({
|
||||
primaryMode: mode,
|
||||
docProps,
|
||||
});
|
||||
|
||||
resolve({ docId: newDoc.id });
|
||||
return;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: {
|
||||
i18nKey: 'com.affine.cmdk.insert-links',
|
||||
},
|
||||
placeholder: {
|
||||
i18nKey: 'com.affine.cmdk.docs.placeholder',
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return searchResult;
|
||||
},
|
||||
});
|
||||
|
||||
const SlashMenuQuickSearchExtension = patchSpecService(
|
||||
'affine:page',
|
||||
(component: WidgetComponent) => {
|
||||
if (component instanceof AffineSlashMenuWidget) {
|
||||
component.config.items.forEach(item => {
|
||||
if (
|
||||
'action' in item &&
|
||||
(item.name === 'Linked Doc' || item.name === 'Link')
|
||||
) {
|
||||
item.action = async ({ rootComponent }) => {
|
||||
const [success, { insertedLinkType }] =
|
||||
rootComponent.std.command.exec(insertLinkByQuickSearchCommand);
|
||||
|
||||
if (!success) return;
|
||||
|
||||
insertedLinkType
|
||||
?.then(type => {
|
||||
const flavour = type?.flavour;
|
||||
if (!flavour) return;
|
||||
|
||||
if (flavour === 'affine:bookmark') {
|
||||
track.doc.editor.slashMenu.bookmark();
|
||||
return;
|
||||
}
|
||||
|
||||
if (flavour === 'affine:embed-linked-doc') {
|
||||
track.doc.editor.slashMenu.linkDoc({
|
||||
control: 'linkDoc',
|
||||
});
|
||||
return;
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
return [QuickSearch, SlashMenuQuickSearchExtension];
|
||||
}
|
||||
|
||||
function patchSpecService(
|
||||
flavour: string,
|
||||
onWidgetConnected?: (component: WidgetComponent) => void
|
||||
) {
|
||||
class TempServiceWatcher extends BlockServiceWatcher {
|
||||
static override readonly flavour = flavour;
|
||||
override mounted() {
|
||||
super.mounted();
|
||||
const disposableGroup = this.blockService.disposables;
|
||||
if (onWidgetConnected) {
|
||||
disposableGroup.add(
|
||||
this.blockService.specSlots.widgetConnected.on(({ component }) => {
|
||||
onWidgetConnected(component);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return TempServiceWatcher;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { ElementOrFactory } from '@affine/component';
|
||||
import type { AffineReference } from '@blocksuite/affine/blocks';
|
||||
import { ReferenceNodeConfigExtension } from '@blocksuite/affine/blocks';
|
||||
import { type ExtensionType } from '@blocksuite/affine/store';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
export type ReferenceReactRenderer = (
|
||||
reference: AffineReference
|
||||
) => React.ReactElement;
|
||||
|
||||
export function patchReferenceRenderer(
|
||||
reactToLit: (element: ElementOrFactory) => TemplateResult,
|
||||
reactRenderer: ReferenceReactRenderer
|
||||
): ExtensionType {
|
||||
const customContent = (reference: AffineReference) => {
|
||||
const node = reactRenderer(reference);
|
||||
return reactToLit(node);
|
||||
};
|
||||
|
||||
return ReferenceNodeConfigExtension({
|
||||
customContent,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { SidebarExtension } from '@blocksuite/affine/blocks';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
export function patchSideBarService(framework: FrameworkProvider) {
|
||||
const { workbench } = framework.get(WorkbenchService);
|
||||
|
||||
return SidebarExtension({
|
||||
open: (tabId?: string) => {
|
||||
workbench.openSidebar();
|
||||
workbench.activeView$.value.activeSidebarTab(tabId ?? null);
|
||||
},
|
||||
close: () => {
|
||||
workbench.closeSidebar();
|
||||
},
|
||||
getTabIds: () => {
|
||||
return workbench.activeView$.value.sidebarTabs$.value.map(tab => tab.id);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { mixpanel } from '@affine/track';
|
||||
import {
|
||||
type TelemetryEventMap,
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import type { ExtensionType } from '@blocksuite/affine/store';
|
||||
|
||||
export function getTelemetryExtension(): ExtensionType {
|
||||
return {
|
||||
setup: di => {
|
||||
di.addImpl(TelemetryProvider, () => ({
|
||||
track: <T extends keyof TelemetryEventMap>(
|
||||
eventName: T,
|
||||
props: TelemetryEventMap[T]
|
||||
) => {
|
||||
mixpanel.track(eventName as string, props as Record<string, unknown>);
|
||||
},
|
||||
}));
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { DocService, DocsService } from '@affine/core/modules/doc';
|
||||
import { AppThemeService } from '@affine/core/modules/theme';
|
||||
import { LifeCycleWatcher, StdIdentifier } from '@blocksuite/affine/block-std';
|
||||
import type { Signal, ThemeExtension } from '@blocksuite/affine/blocks';
|
||||
import {
|
||||
ColorScheme,
|
||||
createSignalFromObservable,
|
||||
ThemeExtensionIdentifier,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import type { Container } from '@blocksuite/affine/global/di';
|
||||
import { type FrameworkProvider } from '@toeverything/infra';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { combineLatest, map } from 'rxjs';
|
||||
|
||||
export function getThemeExtension(
|
||||
framework: FrameworkProvider
|
||||
): typeof LifeCycleWatcher {
|
||||
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 || 'system'
|
||||
);
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user