feat(core): use new print pdf api (#7932)

This commit is contained in:
EYHN 2024-08-21 10:06:22 +00:00
parent cf086e4018
commit 3db95bafa2
No known key found for this signature in database
GPG Key ID: 46C9E26A75AB276C
17 changed files with 197 additions and 118 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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,

View File

@ -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,

View File

@ -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 (
<>
<div className={styles.exportContainerStyle}>
<div className={styles.descriptionStyle}>
{t['com.affine.share-menu.ShareViaExportDescription']()}
</div>
@ -25,6 +27,19 @@ export const ShareExport = ({ currentPage }: ShareMenuProps) => {
pageMode={currentMode}
/>
</div>
</>
{currentMode === 'page' && (
<>
<div className={styles.descriptionStyle}>
{t['com.affine.share-menu.ShareViaPrintDescription']()}
</div>
<div>
<PrintMenuItems
exportHandler={exportHandler}
className={styles.exportItemStyle}
/>
</div>
</>
)}
</div>
);
};

View File

@ -39,7 +39,7 @@ export const ShareMenuContent = (props: ShareMenuProps) => {
<SharePage {...props} />
</Tabs.Content>
<Tabs.Content value="export">
<ShareExport {...props} />
<ShareExport />
</Tabs.Content>
</Tabs.Root>
</div>

View File

@ -14,6 +14,11 @@ export const container = style({
padding: '0 24px',
},
},
'@media': {
print: {
display: 'none',
},
},
});
export const dividerContainer = style({

View File

@ -168,6 +168,9 @@ export const BlocksuiteEditorContainer = forwardRef<
get mode() {
return mode;
},
get origin() {
return rootRef.current;
},
};
const proxy = new Proxy(api, {

View File

@ -162,7 +162,7 @@ export const PageHeaderMenuButton = ({
}
}, []);
const exportHandler = useExportPage(editorService.editor.doc.blockSuiteDoc);
const exportHandler = useExportPage();
const handleDuplicate = useCallback(() => {
duplicate(pageId);

View File

@ -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<T> {
}
interface ExportProps {
exportHandler: (type: 'pdf' | 'html' | 'png' | 'markdown') => Promise<void>;
exportHandler: (type: 'pdf' | 'html' | 'png' | 'markdown') => void;
pageMode?: 'page' | 'edgeless';
className?: string;
}
@ -47,74 +47,73 @@ export function ExportMenuItem<T>({
);
}
export const PrintMenuItems = ({
exportHandler,
className = transitionStyle,
}: ExportProps) => {
const t = useI18n();
return (
<ExportMenuItem
onSelect={() => exportHandler('pdf')}
className={className}
type="pdf"
icon={<FileIcon />}
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: <ExportToPdfIcon />,
label: t['Export to PDF'](),
},
},
{
component: ExportMenuItem,
props: {
onSelect: () => exportHandler('html'),
className: className,
type: 'html',
icon: <ExportToHtmlIcon />,
label: t['Export to HTML'](),
},
},
{
component: ExportMenuItem,
props: {
onSelect: () => exportHandler('png'),
className: className,
type: 'png',
icon: <ExportToPngIcon />,
label: t['Export to PNG'](),
},
},
{
component: ExportMenuItem,
props: {
onSelect: () => exportHandler('markdown'),
className: className,
type: 'markdown',
icon: <ExportToMarkdownIcon />,
label: t['Export to Markdown'](),
},
},
],
[className, exportHandler, t]
return (
<>
<ExportMenuItem
onSelect={() => exportHandler('html')}
className={className}
type="html"
icon={<ExportToHtmlIcon />}
label={t['Export to HTML']()}
/>
{pageMode !== 'edgeless' && (
<ExportMenuItem
onSelect={() => exportHandler('png')}
className={className}
type="png"
icon={<ExportToPngIcon />}
label={t['Export to PNG']()}
/>
)}
<ExportMenuItem
onSelect={() => exportHandler('markdown')}
className={className}
type="markdown"
icon={<ExportToMarkdownIcon />}
label={t['Export to Markdown']()}
/>
</>
);
const items = itemMap.map(({ component: Component, props }) =>
pageMode === 'edgeless' &&
(props.type === 'pdf' || props.type === 'png') ? null : (
<Component key={props.label} {...props} />
)
);
return items;
};
export const Export = ({ exportHandler, className, pageMode }: ExportProps) => {
const t = useI18n();
const items = (
<ExportMenuItems
exportHandler={exportHandler}
className={className}
pageMode={pageMode}
/>
<>
<ExportMenuItems
exportHandler={exportHandler}
className={className}
pageMode={pageMode}
/>
{pageMode !== 'edgeless' && (
<>
<MenuSeparator />
<PrintMenuItems exportHandler={exportHandler} className={className} />
</>
)}
</>
);
const handleExportMenuOpenChange = useCallback((open: boolean) => {
if (open) {

View File

@ -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;

View File

@ -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');
},
})
);

View File

@ -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<AffineEditorContainer | null>(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

View File

@ -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<AffineEditorContainer | null>(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();

View File

@ -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",

View File

@ -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);

View File

@ -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