feat(core): pdf embed view component (#8671)

Closes: [AF-1445](https://linear.app/affine-design/issue/AF-1445/添加-pdf-embed-view-component)
Upstreams: https://github.com/toeverything/blocksuite/pull/8658, https://github.com/toeverything/blocksuite/pull/8752

### What's Changed

* uses `IntersectionObserver` to determine lazy loading
* adds `PDFViewerEmbedded` component
* automatically shut down workers when out of view

<div class='graphite__hidden'>
          <div>🎥 Video uploaded on Graphite:</div>
            <a href="https://app.graphite.dev/media/video/8ypiIKZXudF5a0tIgIzf/4181bec6-83fd-42f1-bfea-87276e0bd9e6.mov">
              <img src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/8ypiIKZXudF5a0tIgIzf/4181bec6-83fd-42f1-bfea-87276e0bd9e6.mov">
            </a>
          </div>
<video src="https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/8ypiIKZXudF5a0tIgIzf/4181bec6-83fd-42f1-bfea-87276e0bd9e6.mov">Screen Recording 2024-11-15 at 08.14.34.mov</video>
This commit is contained in:
fundon
2024-12-26 05:28:17 +00:00
parent 90173b2fa3
commit bb43e4b664
16 changed files with 646 additions and 248 deletions
@@ -103,8 +103,9 @@ class PDFRendererBackend extends OpConsumer<ClientOps> {
if (!page) return;
const width = Math.ceil(opts.width * (opts.scale ?? 1));
const height = Math.ceil(opts.height * (opts.scale ?? 1));
const scale = opts.scale ?? 1;
const width = Math.ceil(opts.width * scale);
const height = Math.ceil(opts.height * scale);
const bitmap = viewer.createBitmap(width, height, 0);
bitmap.fill(0, 0, width, height);
@@ -119,9 +120,8 @@ class PDFRendererBackend extends OpConsumer<ClientOps> {
);
const data = new Uint8ClampedArray(bitmap.toUint8Array());
const imageBitmap = await createImageBitmap(
new ImageData(data, width, height)
);
const imageData = new ImageData(data, width, height);
const imageBitmap = await createImageBitmap(imageData);
bitmap.close();
page.close();
@@ -113,3 +113,9 @@ export const LoadingSvg = memo(
);
LoadingSvg.displayName = 'pdf-loading';
export const PDFPageCanvas = forwardRef<HTMLCanvasElement>((props, ref) => {
return <canvas className={styles.pdfPageCanvas} ref={ref} {...props} />;
});
PDFPageCanvas.displayName = 'pdf-page-canvas';
@@ -4,6 +4,7 @@ export {
ListPadding,
ListWithSmallGap,
LoadingSvg,
PDFPageCanvas,
type PDFVirtuosoContext,
type PDFVirtuosoProps,
Scroller,
@@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from 'react';
import type { PDF } from '../entities/pdf';
import type { PDFPage } from '../entities/pdf-page';
import { LoadingSvg } from './components';
import { LoadingSvg, PDFPageCanvas } from './components';
import * as styles from './styles.css';
interface PDFPageProps {
@@ -34,6 +34,8 @@ export const PDFPageRenderer = ({
const style = { width, aspectRatio: `${width} / ${height}` };
useEffect(() => {
if (width * height === 0) return;
const { page, release } = pdf.page(pageNum, `${width}:${height}:${scale}`);
setPdfPage(page);
@@ -41,6 +43,8 @@ export const PDFPageRenderer = ({
}, [pdf, width, height, pageNum, scale]);
useEffect(() => {
if (width * height === 0) return;
pdfPage?.render({ width, height, scale });
return pdfPage?.render.unsubscribe;
@@ -52,6 +56,7 @@ export const PDFPageRenderer = ({
if (!img) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
if (width * height === 0) return;
canvas.width = width * scale;
canvas.height = height * scale;
@@ -74,11 +79,7 @@ export const PDFPageRenderer = ({
style={style}
onClick={() => onSelect?.(pageNum)}
>
{img === null ? (
<LoadingSvg />
) : (
<canvas className={styles.pdfPageCanvas} ref={canvasRef} />
)}
{img === null ? <LoadingSvg /> : <PDFPageCanvas ref={canvasRef} />}
</div>
);
};
@@ -25,18 +25,6 @@ export const virtuosoItem = style({
justifyContent: 'center',
});
export const pdfPage = style({
overflow: 'hidden',
maxWidth: 'calc(100% - 40px)',
background: cssVarV2('layer/white'),
boxSizing: 'border-box',
borderWidth: '1px',
borderStyle: 'solid',
borderColor: cssVarV2('layer/insideBorder/border'),
boxShadow:
'0px 4px 20px 0px var(--transparent-black-200, rgba(0, 0, 0, 0.10))',
});
export const pdfPageError = style({
display: 'flex',
alignSelf: 'center',
@@ -62,4 +50,5 @@ export const pdfLoading = style({
width: '100%',
height: '100%',
maxWidth: '537px',
overflow: 'hidden',
});
@@ -9,7 +9,7 @@ import type {
SurfaceRefBlockModel,
} from '@blocksuite/affine/blocks';
import { AffineReference } from '@blocksuite/affine/blocks';
import type { BlockModel } from '@blocksuite/affine/store';
import type { Block, BlockModel } from '@blocksuite/affine/store';
import type { AIChatBlockModel } from '@toeverything/infra';
import { Entity, LiveData } from '@toeverything/infra';
import type { TemplateResult } from 'lit';
@@ -36,7 +36,8 @@ export type PeekViewElement =
| HTMLElement
| BlockComponent
| AffineReference
| HTMLAnchorElement;
| HTMLAnchorElement
| Block;
export interface PeekViewTarget {
element?: PeekViewElement;
@@ -184,7 +185,7 @@ function resolvePeekInfoFromPeekTarget(
blockIds: [blockModel.id],
},
};
} else if (isAIChatBlockModel(blockModel)) {
} else if (isAIChatBlockModel(blockModel) && 'host' in element) {
return {
type: 'ai-chat-block',
docRef: {