mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-02 06:39:46 +08:00
feat(core): add editor commanads (#4514)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
This commit is contained in:
@@ -2,8 +2,11 @@ import {
|
||||
useBlockSuitePageMeta,
|
||||
usePageMetaHelper,
|
||||
} from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { setPageModeAtom } from '../../atoms';
|
||||
import { currentModeAtom } from '../../atoms/mode';
|
||||
import type { BlockSuiteWorkspace } from '../../shared';
|
||||
import { useReferenceLinkHelper } from './use-reference-link-helper';
|
||||
|
||||
@@ -14,6 +17,27 @@ export function useBlockSuiteMetaHelper(
|
||||
usePageMetaHelper(blockSuiteWorkspace);
|
||||
const { addReferenceLink } = useReferenceLinkHelper(blockSuiteWorkspace);
|
||||
const metas = useBlockSuitePageMeta(blockSuiteWorkspace);
|
||||
const setPageMode = useSetAtom(setPageModeAtom);
|
||||
const currentMode = useAtomValue(currentModeAtom);
|
||||
|
||||
const switchToPageMode = useCallback(
|
||||
(pageId: string) => {
|
||||
setPageMode(pageId, 'page');
|
||||
},
|
||||
[setPageMode]
|
||||
);
|
||||
const switchToEdgelessMode = useCallback(
|
||||
(pageId: string) => {
|
||||
setPageMode(pageId, 'edgeless');
|
||||
},
|
||||
[setPageMode]
|
||||
);
|
||||
const togglePageMode = useCallback(
|
||||
(pageId: string) => {
|
||||
setPageMode(pageId, currentMode === 'edgeless' ? 'page' : 'edgeless');
|
||||
},
|
||||
[currentMode, setPageMode]
|
||||
);
|
||||
|
||||
const addToFavorite = useCallback(
|
||||
(pageId: string) => {
|
||||
@@ -115,6 +139,10 @@ export function useBlockSuiteMetaHelper(
|
||||
);
|
||||
|
||||
return {
|
||||
switchToPageMode,
|
||||
switchToEdgelessMode,
|
||||
togglePageMode,
|
||||
|
||||
publicPage,
|
||||
cancelPublicPage,
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { PageBlockModel } from '@blocksuite/blocks';
|
||||
import { ContentParser } from '@blocksuite/blocks/content-parser';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
type ExportType = 'pdf' | 'html' | 'png' | 'markdown';
|
||||
const typeToContentParserMethodMap = {
|
||||
pdf: 'exportPdf',
|
||||
html: 'exportHtml',
|
||||
png: 'exportPng',
|
||||
markdown: 'exportMarkdown',
|
||||
} satisfies Record<ExportType, keyof ContentParser>;
|
||||
|
||||
const contentParserWeakMap = new WeakMap<Page, ContentParser>();
|
||||
|
||||
const getContentParser = (page: Page) => {
|
||||
if (!contentParserWeakMap.has(page)) {
|
||||
contentParserWeakMap.set(
|
||||
page,
|
||||
new ContentParser(page, {
|
||||
imageProxyEndpoint: !environment.isDesktop
|
||||
? runtimeConfig.imageProxyUrl
|
||||
: undefined,
|
||||
})
|
||||
);
|
||||
}
|
||||
return contentParserWeakMap.get(page) as ContentParser;
|
||||
};
|
||||
|
||||
interface ExportHandlerOptions {
|
||||
page: Page;
|
||||
type: ExportType;
|
||||
}
|
||||
|
||||
async function exportHandler({ page, type }: ExportHandlerOptions) {
|
||||
if (type === 'pdf' && environment.isDesktop && page.meta.mode === 'page') {
|
||||
window.apis?.export.savePDFFileAs(
|
||||
(page.root as PageBlockModel).title.toString()
|
||||
);
|
||||
} else {
|
||||
const contentParser = getContentParser(page);
|
||||
const method = typeToContentParserMethodMap[type];
|
||||
await contentParser[method]();
|
||||
}
|
||||
}
|
||||
|
||||
export const useExportPage = (page: Page) => {
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const onClickHandler = useCallback(
|
||||
async (type: ExportType) => {
|
||||
try {
|
||||
await exportHandler({
|
||||
page,
|
||||
type,
|
||||
});
|
||||
pushNotification({
|
||||
title: t['com.affine.export.success.title'](),
|
||||
message: t['com.affine.export.success.message'](),
|
||||
type: 'success',
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
pushNotification({
|
||||
title: t['com.affine.export.error.title'](),
|
||||
message: t['com.affine.export.error.message'](),
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
},
|
||||
[page, pushNotification, t]
|
||||
);
|
||||
|
||||
return onClickHandler;
|
||||
};
|
||||
@@ -0,0 +1,208 @@
|
||||
import { toast } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import {
|
||||
PreconditionStrategy,
|
||||
registerAffineCommand,
|
||||
} from '@toeverything/infra/command';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
|
||||
import { useExportPage } from './use-export-page';
|
||||
import { useTrashModalHelper } from './use-trash-modal-helper';
|
||||
|
||||
export function useRegisterBlocksuiteEditorCommands(
|
||||
blockSuiteWorkspace: Workspace,
|
||||
pageId: string,
|
||||
mode: 'page' | 'edgeless'
|
||||
) {
|
||||
const t = useAFFiNEI18N();
|
||||
const { getPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
|
||||
const currentPage = blockSuiteWorkspace.getPage(pageId);
|
||||
assertExists(currentPage);
|
||||
const pageMeta = getPageMeta(pageId);
|
||||
assertExists(pageMeta);
|
||||
const favorite = pageMeta.favorite ?? false;
|
||||
const trash = pageMeta.trash ?? false;
|
||||
|
||||
const { togglePageMode, toggleFavorite, restoreFromTrash } =
|
||||
useBlockSuiteMetaHelper(blockSuiteWorkspace);
|
||||
const exportHandler = useExportPage(currentPage);
|
||||
const { setTrashModal } = useTrashModalHelper(blockSuiteWorkspace);
|
||||
const onClickDelete = useCallback(() => {
|
||||
setTrashModal({
|
||||
open: true,
|
||||
pageId: pageId,
|
||||
pageTitle: pageMeta.title,
|
||||
});
|
||||
}, [pageId, pageMeta.title, setTrashModal]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubs: Array<() => void> = [];
|
||||
const preconditionStrategy = () =>
|
||||
PreconditionStrategy.InPaperOrEdgeless && !trash;
|
||||
|
||||
//TODO: add back when edgeless presentation is ready
|
||||
|
||||
// this is pretty hack and easy to break. need a better way to communicate with blocksuite editor
|
||||
// unsubs.push(
|
||||
// registerAffineCommand({
|
||||
// id: 'editor:edgeless-presentation-start',
|
||||
// preconditionStrategy: () => PreconditionStrategy.InEdgeless && !trash,
|
||||
// category: 'editor:edgeless',
|
||||
// icon: <EdgelessIcon />,
|
||||
// label: t['com.affine.cmdk.affine.editor.edgeless.presentation-start'](),
|
||||
// run() {
|
||||
// document
|
||||
// .querySelector<HTMLElement>('edgeless-toolbar')
|
||||
// ?.shadowRoot?.querySelector<HTMLElement>(
|
||||
// '.edgeless-toolbar-left-part > edgeless-tool-icon-button:last-child'
|
||||
// )
|
||||
// ?.click();
|
||||
// },
|
||||
// })
|
||||
// );
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: `editor:${mode}-${favorite ? 'remove-from' : 'add-to'}-favourites`,
|
||||
preconditionStrategy,
|
||||
category: `editor:${mode}`,
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: favorite
|
||||
? t['com.affine.favoritePageOperation.remove']()
|
||||
: t['com.affine.favoritePageOperation.add'](),
|
||||
run() {
|
||||
toggleFavorite(pageId);
|
||||
toast(
|
||||
favorite
|
||||
? t['com.affine.cmdk.affine.editor.remove-from-favourites']()
|
||||
: t['com.affine.cmdk.affine.editor.add-to-favourites']()
|
||||
);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: `editor:${mode}-convert-to-${
|
||||
mode === 'page' ? 'edgeless' : 'page'
|
||||
}`,
|
||||
preconditionStrategy,
|
||||
category: `editor:${mode}`,
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: `${t['Convert to ']()}${
|
||||
mode === 'page'
|
||||
? t['com.affine.pageMode.edgeless']()
|
||||
: t['com.affine.pageMode.page']()
|
||||
}`,
|
||||
run() {
|
||||
togglePageMode(pageId);
|
||||
toast(
|
||||
mode === 'page'
|
||||
? t['com.affine.toastMessage.edgelessMode']()
|
||||
: t['com.affine.toastMessage.pageMode']()
|
||||
);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: `editor:${mode}-export-to-pdf`,
|
||||
preconditionStrategy,
|
||||
category: `editor:${mode}`,
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: t['Export to PDF'](),
|
||||
run() {
|
||||
exportHandler('pdf');
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: `editor:${mode}-export-to-html`,
|
||||
preconditionStrategy,
|
||||
category: `editor:${mode}`,
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: t['Export to HTML'](),
|
||||
run() {
|
||||
exportHandler('html');
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: `editor:${mode}-export-to-png`,
|
||||
preconditionStrategy,
|
||||
category: `editor:${mode}`,
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: t['Export to PNG'](),
|
||||
run() {
|
||||
exportHandler('png');
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: `editor:${mode}-export-to-markdown`,
|
||||
preconditionStrategy,
|
||||
category: `editor:${mode}`,
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: t['Export to Markdown'](),
|
||||
run() {
|
||||
exportHandler('markdown');
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: `editor:${mode}-move-to-trash`,
|
||||
preconditionStrategy,
|
||||
category: `editor:${mode}`,
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: t['com.affine.moveToTrash.title'](),
|
||||
run() {
|
||||
onClickDelete();
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: `editor:${mode}-restore-from-trash`,
|
||||
preconditionStrategy: () =>
|
||||
PreconditionStrategy.InPaperOrEdgeless && trash,
|
||||
category: `editor:${mode}`,
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: t['com.affine.cmdk.affine.editor.restore-from-trash'](),
|
||||
run() {
|
||||
restoreFromTrash(pageId);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubs.forEach(unsub => unsub());
|
||||
};
|
||||
}, [
|
||||
favorite,
|
||||
mode,
|
||||
onClickDelete,
|
||||
exportHandler,
|
||||
pageId,
|
||||
pageMeta.title,
|
||||
restoreFromTrash,
|
||||
t,
|
||||
toggleFavorite,
|
||||
togglePageMode,
|
||||
trash,
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { toast } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { trashModalAtom } from '../../atoms/trash-modal';
|
||||
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
|
||||
|
||||
export function useTrashModalHelper(blocksuiteWorkspace: Workspace) {
|
||||
const t = useAFFiNEI18N();
|
||||
const [trashModal, setTrashModal] = useAtom(trashModalAtom);
|
||||
const { pageId } = trashModal;
|
||||
const { removeToTrash } = useBlockSuiteMetaHelper(blocksuiteWorkspace);
|
||||
|
||||
const handleOnConfirm = useCallback(() => {
|
||||
removeToTrash(pageId);
|
||||
toast(t['com.affine.toastMessage.movedTrash']());
|
||||
setTrashModal({ ...trashModal, open: false });
|
||||
}, [pageId, removeToTrash, setTrashModal, t, trashModal]);
|
||||
|
||||
return {
|
||||
trashModal,
|
||||
setTrashModal,
|
||||
handleOnConfirm,
|
||||
};
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export function useRegisterWorkspaceCommands() {
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
|
||||
const navigationHelper = useNavigateHelper();
|
||||
const [pageMode, setPageMode] = useAtom(allPageModeSelectAtom);
|
||||
const [pageListMode, setPageListMode] = useAtom(allPageModeSelectAtom);
|
||||
useEffect(() => {
|
||||
const unsubs: Array<() => void> = [];
|
||||
unsubs.push(
|
||||
@@ -30,8 +30,8 @@ export function useRegisterWorkspaceCommands() {
|
||||
t,
|
||||
workspace: currentWorkspace.blockSuiteWorkspace,
|
||||
navigationHelper,
|
||||
pageMode,
|
||||
setPageMode,
|
||||
pageListMode,
|
||||
setPageListMode,
|
||||
})
|
||||
);
|
||||
unsubs.push(registerAffineSettingsCommands({ store, t, theme }));
|
||||
@@ -54,7 +54,7 @@ export function useRegisterWorkspaceCommands() {
|
||||
theme,
|
||||
currentWorkspace.blockSuiteWorkspace,
|
||||
navigationHelper,
|
||||
pageMode,
|
||||
setPageMode,
|
||||
pageListMode,
|
||||
setPageListMode,
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user