diff --git a/packages/frontend/component/package.json b/packages/frontend/component/package.json index 97b14161ad..65da9d99a7 100644 --- a/packages/frontend/component/package.json +++ b/packages/frontend/component/package.json @@ -49,7 +49,7 @@ "@radix-ui/react-toolbar": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-visually-hidden": "^1.1.0", - "@toeverything/theme": "^1.0.5", + "@toeverything/theme": "^1.0.7", "@vanilla-extract/dynamic": "^2.1.0", "bytes": "^3.1.2", "check-password-strength": "^2.0.10", diff --git a/packages/frontend/core/package.json b/packages/frontend/core/package.json index c47a44c467..7fca1c8629 100644 --- a/packages/frontend/core/package.json +++ b/packages/frontend/core/package.json @@ -48,7 +48,7 @@ "@sentry/integrations": "^7.109.0", "@sentry/react": "^8.0.0", "@sgtpooki/file-type": "^1.0.1", - "@toeverything/theme": "^1.0.5", + "@toeverything/theme": "^1.0.7", "@vanilla-extract/dynamic": "^2.1.0", "animejs": "^3.2.2", "async-call-rpc": "^6.4.2", diff --git a/packages/frontend/core/src/components/affine/page-properties/styles.css.ts b/packages/frontend/core/src/components/affine/page-properties/styles.css.ts index a00d387210..fcccab3ebf 100644 --- a/packages/frontend/core/src/components/affine/page-properties/styles.css.ts +++ b/packages/frontend/core/src/components/affine/page-properties/styles.css.ts @@ -42,6 +42,11 @@ export const tableHeaderInfoRow = style({ fontSize: cssVar('fontSm'), fontWeight: 500, minHeight: 34, + '@media': { + print: { + display: 'none', + }, + }, }); export const tableHeaderSecondaryRow = style({ @@ -54,6 +59,11 @@ export const tableHeaderSecondaryRow = style({ padding: '0 6px', gap: '8px', height: 24, + '@media': { + print: { + display: 'none', + }, + }, }); export const tableHeaderCollapseButtonWrapper = style({ @@ -101,12 +111,26 @@ export const tableHeaderDivider = style({ borderTop: `0.5px solid ${cssVar('borderColor')}`, width: '100%', margin: '8px 0', + '@media': { + print: { + display: 'none', + }, + }, }); export const tableBodyRoot = style({ display: 'flex', flexDirection: 'column', gap: 8, + '@media': { + print: { + selectors: { + '&[data-state="open"]': { + marginBottom: 32, + }, + }, + }, + }, }); export const tableBodySortable = style({ @@ -124,6 +148,11 @@ export const addPropertyButton = style({ height: 36, fontWeight: 400, gap: 6, + '@media': { + print: { + display: 'none', + }, + }, }); globalStyle(`${addPropertyButton} svg`, { fontSize: 16, diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/index.css.ts b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/index.css.ts index bdc6cf817a..fbe34c0c58 100644 --- a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/index.css.ts +++ b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/index.css.ts @@ -148,6 +148,11 @@ export const rowContainerStyle = style({ alignItems: 'center', padding: '4px', }); +export const exportContainerStyle = style({ + display: 'flex', + flexDirection: 'column', + gap: '8px', +}); export const labelStyle = style({ fontSize: cssVar('fontSm'), fontWeight: 500, diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-export.tsx b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-export.tsx index 1967a1ecf2..45eee2a512 100644 --- a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-export.tsx +++ b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-export.tsx @@ -1,20 +1,22 @@ -import { ExportMenuItems } from '@affine/core/components/page-list'; +import { + ExportMenuItems, + PrintMenuItems, +} from '@affine/core/components/page-list'; import { useExportPage } from '@affine/core/hooks/affine/use-export-page'; import { EditorService } from '@affine/core/modules/editor'; import { useI18n } from '@affine/i18n'; import { useLiveData, useService } from '@toeverything/infra'; import * as styles from './index.css'; -import type { ShareMenuProps } from './share-menu'; -export const ShareExport = ({ currentPage }: ShareMenuProps) => { +export const ShareExport = () => { const t = useI18n(); const editor = useService(EditorService).editor; - const exportHandler = useExportPage(currentPage); + const exportHandler = useExportPage(); const currentMode = useLiveData(editor.mode$); return ( - <> +
{t['com.affine.share-menu.ShareViaExportDescription']()}
@@ -25,6 +27,19 @@ export const ShareExport = ({ currentPage }: ShareMenuProps) => { pageMode={currentMode} />
- + {currentMode === 'page' && ( + <> +
+ {t['com.affine.share-menu.ShareViaPrintDescription']()} +
+
+ +
+ + )} + ); }; diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx index c0ecb70f38..246db6153b 100644 --- a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx +++ b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx @@ -39,7 +39,7 @@ export const ShareMenuContent = (props: ShareMenuProps) => { - + diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.css.ts b/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.css.ts index 936555e61d..1c4244c329 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.css.ts +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.css.ts @@ -14,6 +14,11 @@ export const container = style({ padding: '0 24px', }, }, + '@media': { + print: { + display: 'none', + }, + }, }); export const dividerContainer = style({ diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/blocksuite-editor-container.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/blocksuite-editor-container.tsx index a09517b642..2ed15c5a80 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/blocksuite-editor-container.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/blocksuite-editor-container.tsx @@ -168,6 +168,9 @@ export const BlocksuiteEditorContainer = forwardRef< get mode() { return mode; }, + get origin() { + return rootRef.current; + }, }; const proxy = new Proxy(api, { diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx index 86b1857190..559f449428 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx @@ -162,7 +162,7 @@ export const PageHeaderMenuButton = ({ } }, []); - const exportHandler = useExportPage(editorService.editor.doc.blockSuiteDoc); + const exportHandler = useExportPage(); const handleDuplicate = useCallback(() => { duplicate(pageId); diff --git a/packages/frontend/core/src/components/page-list/operation-menu-items/export.tsx b/packages/frontend/core/src/components/page-list/operation-menu-items/export.tsx index 893fd95a8c..f57931fd65 100644 --- a/packages/frontend/core/src/components/page-list/operation-menu-items/export.tsx +++ b/packages/frontend/core/src/components/page-list/operation-menu-items/export.tsx @@ -1,15 +1,15 @@ -import { MenuItem, MenuSub } from '@affine/component'; +import { MenuItem, MenuSeparator, MenuSub } from '@affine/component'; import { track } from '@affine/core/mixpanel'; import { useI18n } from '@affine/i18n'; import { ExportIcon, ExportToHtmlIcon, ExportToMarkdownIcon, - ExportToPdfIcon, ExportToPngIcon, + FileIcon, } from '@blocksuite/icons/rc'; import type { ReactNode } from 'react'; -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { transitionStyle } from './index.css'; @@ -22,7 +22,7 @@ interface ExportMenuItemProps { } interface ExportProps { - exportHandler: (type: 'pdf' | 'html' | 'png' | 'markdown') => Promise; + exportHandler: (type: 'pdf' | 'html' | 'png' | 'markdown') => void; pageMode?: 'page' | 'edgeless'; className?: string; } @@ -47,74 +47,73 @@ export function ExportMenuItem({ ); } +export const PrintMenuItems = ({ + exportHandler, + className = transitionStyle, +}: ExportProps) => { + const t = useI18n(); + return ( + exportHandler('pdf')} + className={className} + type="pdf" + icon={} + label={t['com.affine.export.print']()} + /> + ); +}; + export const ExportMenuItems = ({ exportHandler, className = transitionStyle, pageMode = 'page', }: ExportProps) => { const t = useI18n(); - const itemMap = useMemo( - () => [ - { - component: ExportMenuItem, - props: { - onSelect: () => exportHandler('pdf'), - className: className, - type: 'pdf', - icon: , - label: t['Export to PDF'](), - }, - }, - { - component: ExportMenuItem, - props: { - onSelect: () => exportHandler('html'), - className: className, - type: 'html', - icon: , - label: t['Export to HTML'](), - }, - }, - { - component: ExportMenuItem, - props: { - onSelect: () => exportHandler('png'), - className: className, - type: 'png', - icon: , - label: t['Export to PNG'](), - }, - }, - { - component: ExportMenuItem, - props: { - onSelect: () => exportHandler('markdown'), - className: className, - type: 'markdown', - icon: , - label: t['Export to Markdown'](), - }, - }, - ], - [className, exportHandler, t] + return ( + <> + exportHandler('html')} + className={className} + type="html" + icon={} + label={t['Export to HTML']()} + /> + {pageMode !== 'edgeless' && ( + exportHandler('png')} + className={className} + type="png" + icon={} + label={t['Export to PNG']()} + /> + )} + exportHandler('markdown')} + className={className} + type="markdown" + icon={} + label={t['Export to Markdown']()} + /> + ); - const items = itemMap.map(({ component: Component, props }) => - pageMode === 'edgeless' && - (props.type === 'pdf' || props.type === 'png') ? null : ( - - ) - ); - return items; }; export const Export = ({ exportHandler, className, pageMode }: ExportProps) => { const t = useI18n(); const items = ( - + <> + + {pageMode !== 'edgeless' && ( + <> + + + + )} + ); const handleExportMenuOpenChange = useCallback((open: boolean) => { if (open) { diff --git a/packages/frontend/core/src/hooks/affine/use-export-page.ts b/packages/frontend/core/src/hooks/affine/use-export-page.ts index 8e66017658..7d9e0da53e 100644 --- a/packages/frontend/core/src/hooks/affine/use-export-page.ts +++ b/packages/frontend/core/src/hooks/affine/use-export-page.ts @@ -4,23 +4,35 @@ import { resolveGlobalLoadingEventAtom, } from '@affine/component/global-loading'; import { track } from '@affine/core/mixpanel'; -import { apis } from '@affine/electron-api'; +import { EditorService } from '@affine/core/modules/editor'; import { useI18n } from '@affine/i18n'; -import type { PageRootService, RootBlockModel } from '@blocksuite/blocks'; -import { HtmlTransformer, MarkdownTransformer } from '@blocksuite/blocks'; +import type { PageRootService } from '@blocksuite/blocks'; +import { + HtmlTransformer, + MarkdownTransformer, + printToPdf, +} from '@blocksuite/blocks'; +import type { AffineEditorContainer } from '@blocksuite/presets'; import type { Doc } from '@blocksuite/store'; +import { useLiveData, useService } from '@toeverything/infra'; import { useSetAtom } from 'jotai'; import { nanoid } from 'nanoid'; -import { useCallback } from 'react'; + +import { useAsyncCallback } from '../affine-async-hooks'; type ExportType = 'pdf' | 'html' | 'png' | 'markdown'; interface ExportHandlerOptions { page: Doc; + editorContainer: AffineEditorContainer; type: ExportType; } -async function exportHandler({ page, type }: ExportHandlerOptions) { +async function exportHandler({ + page, + type, + editorContainer, +}: ExportHandlerOptions) { const editorRoot = document.querySelector('editor-host'); let pageService: PageRootService | null = null; if (editorRoot) { @@ -37,15 +49,8 @@ async function exportHandler({ page, type }: ExportHandlerOptions) { await MarkdownTransformer.exportDoc(page); break; case 'pdf': - if (environment.isDesktop && page.meta?.mode === 'page') { - await apis?.export.savePDFFileAs( - (page.root as RootBlockModel).title.toString() - ); - } else { - if (!pageService) return; - await pageService.exportManager.exportPdf(); - } - break; + await printToPdf(editorContainer); + return; case 'png': { if (!pageService) return; await pageService.exportManager.exportPng(); @@ -54,21 +59,31 @@ async function exportHandler({ page, type }: ExportHandlerOptions) { } } -export const useExportPage = (page: Doc) => { +export const useExportPage = () => { + const editor = useService(EditorService).editor; + const editorContainer = useLiveData(editor.editorContainer$); + const blocksuiteDoc = editor.doc.blockSuiteDoc; const pushGlobalLoadingEvent = useSetAtom(pushGlobalLoadingEventAtom); const resolveGlobalLoadingEvent = useSetAtom(resolveGlobalLoadingEventAtom); const t = useI18n(); - const onClickHandler = useCallback( + const onClickHandler = useAsyncCallback( async (type: ExportType) => { + if (editorContainer === null) return; + + // editor container is wrapped by a proxy, we need to get the origin + const originEditorContainer = (editorContainer as any) + .origin as AffineEditorContainer; + const globalLoadingID = nanoid(); pushGlobalLoadingEvent({ key: globalLoadingID, }); try { await exportHandler({ - page, + page: blocksuiteDoc, type, + editorContainer: originEditorContainer, }); notify.success({ title: t['com.affine.export.success.title'](), @@ -84,7 +99,13 @@ export const useExportPage = (page: Doc) => { resolveGlobalLoadingEvent(globalLoadingID); } }, - [page, pushGlobalLoadingEvent, resolveGlobalLoadingEvent, t] + [ + blocksuiteDoc, + editorContainer, + pushGlobalLoadingEvent, + resolveGlobalLoadingEvent, + t, + ] ); return onClickHandler; diff --git a/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx b/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx index f6468301ca..b0b1df85e4 100644 --- a/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx +++ b/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx @@ -51,7 +51,7 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) { }, [setInfoModalState]); const { duplicate } = useBlockSuiteMetaHelper(docCollection); - const exportHandler = useExportPage(doc.blockSuiteDoc); + const exportHandler = useExportPage(); const { setTrashModal } = useTrashModalHelper(docCollection); const onClickDelete = useCallback( (title: string) => { @@ -189,7 +189,7 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) { type: 'pdf', }); - await exportHandler('pdf'); + exportHandler('pdf'); }, }) ); @@ -206,7 +206,7 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) { type: 'html', }); - await exportHandler('html'); + exportHandler('html'); }, }) ); @@ -223,7 +223,7 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) { type: 'png', }); - await exportHandler('png'); + exportHandler('png'); }, }) ); @@ -240,7 +240,7 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) { type: 'markdown', }); - await exportHandler('markdown'); + exportHandler('markdown'); }, }) ); diff --git a/packages/frontend/core/src/modules/editor/entities/editor.ts b/packages/frontend/core/src/modules/editor/entities/editor.ts index 3cc26759ca..5feca7fafb 100644 --- a/packages/frontend/core/src/modules/editor/entities/editor.ts +++ b/packages/frontend/core/src/modules/editor/entities/editor.ts @@ -1,4 +1,5 @@ import type { DocMode } from '@blocksuite/blocks'; +import type { AffineEditorContainer } from '@blocksuite/presets'; import type { DocService, WorkspaceService } from '@toeverything/infra'; import { Entity, LiveData } from '@toeverything/infra'; @@ -14,6 +15,8 @@ export class Editor extends Entity<{ defaultMode: DocMode }> { readonly isSharedMode = this.workspaceService.workspace.openOptions.isSharedMode; + readonly editorContainer$ = new LiveData(null); + toggleMode() { this.mode$.next(this.mode$.value === 'edgeless' ? 'page' : 'edgeless'); } @@ -22,6 +25,10 @@ export class Editor extends Entity<{ defaultMode: DocMode }> { this.mode$.next(mode); } + setEditorContainer(editorContainer: AffineEditorContainer | null) { + this.editorContainer$.next(editorContainer); + } + constructor( private readonly docService: DocService, private readonly workspaceService: WorkspaceService diff --git a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx index ae4b8a8638..db02c609bd 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx +++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx @@ -102,8 +102,7 @@ const DetailPageImpl = memo(function DetailPageImpl() { const isInTrash = useLiveData(doc.meta$.map(meta => meta.trash)); const { openPage, jumpToPageBlock, jumpToTag } = useNavigateHelper(); - const [editorContainer, setEditorContainer] = - useState(null); + const editorContainer = useLiveData(editor.editorContainer$); const isSideBarOpen = useLiveData(workbench.sidebarOpen$); const { appSettings } = useAppSettingHelper(); @@ -179,7 +178,7 @@ const DetailPageImpl = memo(function DetailPageImpl() { usePageDocumentTitle(title); const onLoad = useCallback( - (bsPage: BlockSuiteDoc, editor: AffineEditorContainer) => { + (bsPage: BlockSuiteDoc, editorContainer: AffineEditorContainer) => { try { // todo(joooye34): improve the following migration code const surfaceBlock = bsPage.getBlockByFlavour('affine:surface')[0]; @@ -201,7 +200,7 @@ const DetailPageImpl = memo(function DetailPageImpl() { } catch {} // blocksuite editor host - const editorHost = editor.host; + const editorHost = editorContainer.host; // provide image proxy endpoint to blocksuite editorHost?.std.clipboard.use( @@ -240,13 +239,20 @@ const DetailPageImpl = memo(function DetailPageImpl() { ); } - setEditorContainer(editor); + editor.setEditorContainer(editorContainer); return () => { disposable.dispose(); }; }, - [jumpToPageBlock, docCollection.id, openPage, jumpToTag, workspace.id] + [ + editor, + jumpToPageBlock, + docCollection.id, + openPage, + jumpToTag, + workspace.id, + ] ); const [refCallback, hasScrollTop] = useHasScrollTop(); diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 1b02f51319..25ffab4dff 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -975,6 +975,7 @@ "com.affine.payment.billing-setting.upgrade": "Upgrade", "com.affine.payment.billing-setting.view-invoice": "View invoice", "com.affine.payment.billing-setting.year": "year", + "com.affine.export.print": "Print", "com.affine.payment.billing-type-form.description": "Please tell us more about your use case, to make AFFiNE better.", "com.affine.payment.billing-type-form.go": "Go", "com.affine.payment.billing-type-form.title": "Tell us your use case", @@ -1276,6 +1277,7 @@ "com.affine.share-menu.SharePage": "Share doc", "com.affine.share-menu.ShareViaExport": "Share via export", "com.affine.share-menu.ShareViaExportDescription": "Download a static copy of your doc to share with others.", + "com.affine.share-menu.ShareViaPrintDescription": "Print a paper copy.", "com.affine.share-menu.ShareWithLink": "Share with link", "com.affine.share-menu.ShareWithLinkDescription": "Create a link you can easily share with anyone. The visitors will open your doc in the form od a document", "com.affine.share-menu.SharedPage": "Shared doc", diff --git a/tests/affine-local/e2e/quick-search.spec.ts b/tests/affine-local/e2e/quick-search.spec.ts index 12041fce97..e65aa8aef1 100644 --- a/tests/affine-local/e2e/quick-search.spec.ts +++ b/tests/affine-local/e2e/quick-search.spec.ts @@ -336,19 +336,6 @@ test('assert the recent browse pages are on the recent list', async ({ } }); -test('can use cmdk to export pdf', async ({ page }) => { - await openHomePage(page); - await waitForEditorLoad(page); - await clickNewPageButton(page); - await getBlockSuiteEditorTitle(page).click(); - await getBlockSuiteEditorTitle(page).fill('this is a new page to export'); - await openQuickSearchByShortcut(page); - const [download] = await Promise.all([ - page.waitForEvent('download'), - keyboardDownAndSelect(page, 'Export to PDF'), - ]); - expect(download.suggestedFilename()).toBe('this is a new page to export.pdf'); -}); test('can use cmdk to export png', async ({ page }) => { await openHomePage(page); await waitForEditorLoad(page); diff --git a/yarn.lock b/yarn.lock index 6acec17560..437951cb5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -326,7 +326,7 @@ __metadata: "@storybook/react": "npm:^8.2.9" "@storybook/react-vite": "npm:^8.2.9" "@testing-library/react": "npm:^16.0.0" - "@toeverything/theme": "npm:^1.0.5" + "@toeverything/theme": "npm:^1.0.7" "@types/bytes": "npm:^3.1.4" "@types/react": "npm:^18.2.75" "@types/react-dnd": "npm:^3.0.2" @@ -422,7 +422,7 @@ __metadata: "@sgtpooki/file-type": "npm:^1.0.1" "@swc/core": "npm:^1.4.13" "@testing-library/react": "npm:^16.0.0" - "@toeverything/theme": "npm:^1.0.5" + "@toeverything/theme": "npm:^1.0.7" "@types/animejs": "npm:^3.1.12" "@types/bytes": "npm:^3.1.4" "@types/image-blob-reduce": "npm:^4.1.4" @@ -13858,10 +13858,10 @@ __metadata: languageName: unknown linkType: soft -"@toeverything/theme@npm:^1.0.2, @toeverything/theme@npm:^1.0.5": - version: 1.0.5 - resolution: "@toeverything/theme@npm:1.0.5" - checksum: 10/26f177192c546b8b6c953a4d75da5520486fa92c872b889052861cd121ef9840b5006e2f1513988117290066812e08015aa99b025d13c01ca50563a9ba26a732 +"@toeverything/theme@npm:^1.0.2, @toeverything/theme@npm:^1.0.7": + version: 1.0.7 + resolution: "@toeverything/theme@npm:1.0.7" + checksum: 10/86b46af255450ab7ea0a20faf41c27793129852759d23736e914876a696c40e6daa15b25cde7353cd56c673c6191d04cabe6b77f6131ba0b0862bb8d482d7a01 languageName: node linkType: hard