mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
feat: implement export as PDF (#14057)
I used [pdfmake](https://www.npmjs.com/package/pdfmake) to implement an "export as PDF" feature, and I am happy to share with you! This should fix #13577, fix #8846, and fix #13959. A showcase: [Getting Started.pdf](https://github.com/user-attachments/files/24013057/Getting.Started.pdf) Although it might miss rendering some properties currently, it can evolve in the long run and provide a more native experience for the users. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** - Experimental "Export to PDF" option added to the export menu (behind a feature flag) - PDF export supports headings, paragraphs, lists, code blocks, tables, images, callouts, linked documents and embedded content * **Chores** - Added PDF rendering library and consolidated PDF utilities - Feature flag introduced to control rollout * **Tests** - Comprehensive unit tests added for PDF content rendering logic <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: DarkSky <darksky2048@gmail.com>
This commit is contained in:
@@ -297,9 +297,11 @@ export class PlaygroundContent extends SignalWatcher(
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
button.addEventListener('click', handleSendClick);
|
||||
|
||||
this._disposables.add(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
button.removeEventListener('click', handleSendClick);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,25 +4,30 @@ function cleanupUnusedIndexedDB() {
|
||||
return;
|
||||
}
|
||||
|
||||
indexedDB.databases().then(databases => {
|
||||
databases.forEach(database => {
|
||||
if (database.name?.endsWith(':server-clock')) {
|
||||
indexedDB.deleteDatabase(database.name);
|
||||
}
|
||||
if (database.name?.endsWith(':sync-metadata')) {
|
||||
indexedDB.deleteDatabase(database.name);
|
||||
}
|
||||
if (
|
||||
database.name?.startsWith('idx:') &&
|
||||
(database.name.endsWith(':block') || database.name.endsWith(':doc'))
|
||||
) {
|
||||
indexedDB.deleteDatabase(database.name);
|
||||
}
|
||||
if (database.name?.startsWith('jp:')) {
|
||||
indexedDB.deleteDatabase(database.name);
|
||||
}
|
||||
indexedDB
|
||||
.databases()
|
||||
.then(databases => {
|
||||
databases.forEach(database => {
|
||||
if (database.name?.endsWith(':server-clock')) {
|
||||
indexedDB.deleteDatabase(database.name);
|
||||
}
|
||||
if (database.name?.endsWith(':sync-metadata')) {
|
||||
indexedDB.deleteDatabase(database.name);
|
||||
}
|
||||
if (
|
||||
database.name?.startsWith('idx:') &&
|
||||
(database.name.endsWith(':block') || database.name.endsWith(':doc'))
|
||||
) {
|
||||
indexedDB.deleteDatabase(database.name);
|
||||
}
|
||||
if (database.name?.startsWith('jp:')) {
|
||||
indexedDB.deleteDatabase(database.name);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to cleanup unused IndexedDB databases:', error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
cleanupUnusedIndexedDB();
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
download,
|
||||
HtmlTransformer,
|
||||
MarkdownTransformer,
|
||||
PdfTransformer,
|
||||
ZipTransformer,
|
||||
} from '@blocksuite/affine/widgets/linked-doc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
@@ -32,7 +33,13 @@ import { nanoid } from 'nanoid';
|
||||
|
||||
import { useAsyncCallback } from '../affine-async-hooks';
|
||||
|
||||
type ExportType = 'pdf' | 'html' | 'png' | 'markdown' | 'snapshot';
|
||||
type ExportType =
|
||||
| 'pdf'
|
||||
| 'html'
|
||||
| 'png'
|
||||
| 'markdown'
|
||||
| 'snapshot'
|
||||
| 'pdf-export';
|
||||
|
||||
interface ExportHandlerOptions {
|
||||
page: Store;
|
||||
@@ -164,6 +171,10 @@ async function exportHandler({
|
||||
await editorRoot?.std.get(ExportManager).exportPng();
|
||||
return;
|
||||
}
|
||||
case 'pdf-export': {
|
||||
await PdfTransformer.exportDoc(page);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MenuItem, MenuSeparator, MenuSub } from '@affine/component';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import {
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
PageIcon,
|
||||
PrinterIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
@@ -24,7 +26,7 @@ interface ExportMenuItemProps<T> {
|
||||
|
||||
interface ExportProps {
|
||||
exportHandler: (
|
||||
type: 'pdf' | 'html' | 'png' | 'markdown' | 'snapshot'
|
||||
type: 'pdf' | 'html' | 'png' | 'markdown' | 'snapshot' | 'pdf-export'
|
||||
) => void;
|
||||
pageMode?: 'page' | 'edgeless';
|
||||
className?: string;
|
||||
@@ -72,6 +74,11 @@ export const ExportMenuItems = ({
|
||||
pageMode = 'page',
|
||||
}: ExportProps) => {
|
||||
const t = useI18n();
|
||||
const featureFlags = useService(FeatureFlagService).flags;
|
||||
const enable_pdfmake_export = useLiveData(
|
||||
featureFlags.enable_pdfmake_export.$
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExportMenuItem
|
||||
@@ -97,6 +104,15 @@ export const ExportMenuItems = ({
|
||||
icon={<ExportToMarkdownIcon />}
|
||||
label={t['Export to Markdown']()}
|
||||
/>
|
||||
{pageMode !== 'edgeless' && enable_pdfmake_export && (
|
||||
<ExportMenuItem
|
||||
onSelect={() => exportHandler('pdf-export')}
|
||||
className={className}
|
||||
type="pdf-export"
|
||||
icon={<PrinterIcon />}
|
||||
label={t['Export to PDF']()}
|
||||
/>
|
||||
)}
|
||||
<ExportMenuItem
|
||||
onSelect={() => exportHandler('snapshot')}
|
||||
className={className}
|
||||
|
||||
@@ -158,6 +158,7 @@ const McpServerSetting = () => {
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
if (!code) return;
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
navigator.clipboard.writeText(code);
|
||||
notify.success({
|
||||
title: t['Copied to clipboard'](),
|
||||
|
||||
@@ -288,6 +288,15 @@ export const AFFINE_FLAGS = {
|
||||
configurable: isMobile,
|
||||
defaultState: false,
|
||||
},
|
||||
enable_pdfmake_export: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_pdfmake_export',
|
||||
displayName: 'Enable PDF Export',
|
||||
description:
|
||||
'Experimental export PDFs support, it may contain the wrong style.',
|
||||
configurable: true,
|
||||
defaultState: false,
|
||||
},
|
||||
} satisfies { [key in string]: FlagInfo };
|
||||
|
||||
// oxlint-disable-next-line no-redeclare
|
||||
|
||||
Reference in New Issue
Block a user