diff --git a/packages/frontend/core/src/components/attachment-viewer/index.tsx b/packages/frontend/core/src/components/attachment-viewer/index.tsx
index 9ab69a85583c6..3ed74e0f91181 100644
--- a/packages/frontend/core/src/components/attachment-viewer/index.tsx
+++ b/packages/frontend/core/src/components/attachment-viewer/index.tsx
@@ -2,7 +2,7 @@ import { ViewBody, ViewHeader } from '@affine/core/modules/workbench';
import type { AttachmentBlockModel } from '@blocksuite/affine/blocks';
import { AttachmentPreviewErrorBoundary, Error } from './error';
-import { PDFViewer } from './pdf-viewer';
+import { PDFViewer, type PDFViewerProps } from './pdf-viewer';
import * as styles from './styles.css';
import { Titlebar } from './titlebar';
import { buildAttachmentProps } from './utils';
@@ -18,13 +18,7 @@ export const AttachmentViewer = ({ model }: AttachmentViewerProps) => {
return (
- {model.type.endsWith('pdf') ? (
-
-
-
- ) : (
-
- )}
+
);
};
@@ -39,14 +33,18 @@ export const AttachmentViewerView = ({ model }: AttachmentViewerProps) => {
- {model.type.endsWith('pdf') ? (
-
-
-
- ) : (
-
- )}
+
>
);
};
+
+const AttachmentViewerInner = (props: PDFViewerProps) => {
+ return props.model.type.endsWith('pdf') ? (
+
+
+
+ ) : (
+
+ );
+};
diff --git a/packages/frontend/core/src/components/attachment-viewer/pdf-viewer-embedded-inner.tsx b/packages/frontend/core/src/components/attachment-viewer/pdf-viewer-embedded-inner.tsx
new file mode 100644
index 0000000000000..7948f1b0b477f
--- /dev/null
+++ b/packages/frontend/core/src/components/attachment-viewer/pdf-viewer-embedded-inner.tsx
@@ -0,0 +1,249 @@
+import { IconButton, observeIntersection } from '@affine/component';
+import {
+ type PDF,
+ type PDFPage,
+ PDFService,
+ PDFStatus,
+} from '@affine/core/modules/pdf';
+import { LoadingSvg, PDFPageCanvas } from '@affine/core/modules/pdf/views';
+import { PeekViewService } from '@affine/core/modules/peek-view';
+import { stopPropagation } from '@affine/core/utils';
+import {
+ ArrowDownSmallIcon,
+ ArrowUpSmallIcon,
+ AttachmentIcon,
+ CenterPeekIcon,
+} from '@blocksuite/icons/rc';
+import { LiveData, useLiveData, useService } from '@toeverything/infra';
+import clsx from 'clsx';
+import { debounce } from 'lodash-es';
+import {
+ type MouseEvent,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+
+import type { PDFViewerProps } from './pdf-viewer';
+import * as styles from './styles.css';
+import * as embeddedStyles from './styles.embedded.css';
+
+type PDFViewerEmbeddedInnerProps = PDFViewerProps;
+
+export function PDFViewerEmbeddedInner({ model }: PDFViewerEmbeddedInnerProps) {
+ const peekView = useService(PeekViewService).peekView;
+ const pdfService = useService(PDFService);
+ const [pdfEntity, setPdfEntity] = useState<{
+ pdf: PDF;
+ release: () => void;
+ } | null>(null);
+ const [pageEntity, setPageEntity] = useState<{
+ page: PDFPage;
+ release: () => void;
+ } | null>(null);
+
+ const meta = useLiveData(
+ useMemo(() => {
+ return pdfEntity
+ ? pdfEntity.pdf.state$.map(s => {
+ return s.status === PDFStatus.Opened
+ ? s.meta
+ : { pageCount: 0, width: 0, height: 0 };
+ })
+ : new LiveData({ pageCount: 0, width: 0, height: 0 });
+ }, [pdfEntity])
+ );
+ const img = useLiveData(
+ useMemo(() => {
+ return pageEntity ? pageEntity.page.bitmap$ : null;
+ }, [pageEntity])
+ );
+
+ const [isLoading, setIsLoading] = useState(true);
+ const [cursor, setCursor] = useState(0);
+ const viewerRef = useRef(null);
+ const [visibility, setVisibility] = useState(false);
+ const canvasRef = useRef(null);
+
+ const peek = useCallback(() => {
+ const target = model.doc.getBlock(model.id);
+ if (!target) return;
+ peekView.open({ element: target }).catch(console.error);
+ }, [peekView, model]);
+
+ const navigator = useMemo(() => {
+ const p = cursor - 1;
+ const n = cursor + 1;
+
+ return {
+ prev: {
+ disabled: p < 0,
+ onClick: (e: MouseEvent) => {
+ e.stopPropagation();
+ setCursor(p);
+ },
+ },
+ next: {
+ disabled: n >= meta.pageCount,
+ onClick: (e: MouseEvent) => {
+ e.stopPropagation();
+ setCursor(n);
+ },
+ },
+ peek: {
+ onClick: (e: MouseEvent) => {
+ e.stopPropagation();
+ peek();
+ },
+ },
+ };
+ }, [cursor, meta, peek]);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+ if (!img) return;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return;
+ const { width, height } = meta;
+ if (width * height === 0) return;
+
+ setIsLoading(false);
+
+ canvas.width = width * 2;
+ canvas.height = height * 2;
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.drawImage(img, 0, 0);
+ }, [img, meta]);
+
+ useEffect(() => {
+ if (!visibility) return;
+ if (!pageEntity) return;
+
+ const { width, height } = meta;
+ if (width * height === 0) return;
+
+ pageEntity.page.render({ width, height, scale: 2 });
+
+ return () => {
+ pageEntity.page.render.unsubscribe();
+ };
+ }, [visibility, pageEntity, meta]);
+
+ useEffect(() => {
+ if (!visibility) return;
+ if (!pdfEntity) return;
+
+ const { width, height } = meta;
+ if (width * height === 0) return;
+
+ const pageEntity = pdfEntity.pdf.page(cursor, `${width}:${height}:2`);
+
+ setPageEntity(pageEntity);
+
+ return () => {
+ pageEntity.release();
+ setPageEntity(null);
+ };
+ }, [visibility, pdfEntity, cursor, meta]);
+
+ useEffect(() => {
+ if (!visibility) return;
+
+ const pdfEntity = pdfService.get(model);
+
+ setPdfEntity(pdfEntity);
+
+ return () => {
+ pdfEntity.release();
+ setPdfEntity(null);
+ };
+ }, [model, pdfService, visibility]);
+
+ useEffect(() => {
+ const viewer = viewerRef.current;
+ if (!viewer) return;
+
+ return observeIntersection(
+ viewer,
+ debounce(
+ entry => {
+ setVisibility(entry.isIntersecting);
+ },
+ 377,
+ {
+ trailing: true,
+ }
+ )
+ );
+ }, []);
+
+ return (
+
+
+
+
+
+ }
+ className={embeddedStyles.pdfControlButton}
+ onDoubleClick={stopPropagation}
+ {...navigator.prev}
+ />
+ }
+ className={embeddedStyles.pdfControlButton}
+ onDoubleClick={stopPropagation}
+ {...navigator.next}
+ />
+ }
+ className={embeddedStyles.pdfControlButton}
+ onDoubleClick={stopPropagation}
+ {...navigator.peek}
+ />
+
+
+
+
+ );
+}
diff --git a/packages/frontend/core/src/components/attachment-viewer/pdf-viewer-embedded.tsx b/packages/frontend/core/src/components/attachment-viewer/pdf-viewer-embedded.tsx
new file mode 100644
index 0000000000000..a0846beb36536
--- /dev/null
+++ b/packages/frontend/core/src/components/attachment-viewer/pdf-viewer-embedded.tsx
@@ -0,0 +1,14 @@
+import type { AttachmentBlockModel } from '@blocksuite/affine/blocks';
+
+import { PDFViewerEmbeddedInner } from './pdf-viewer-embedded-inner';
+
+export interface PDFViewerEmbeddedProps {
+ model: AttachmentBlockModel;
+ name: string;
+ ext: string;
+ size: string;
+}
+
+export function PDFViewerEmbedded(props: PDFViewerEmbeddedProps) {
+ return ;
+}
diff --git a/packages/frontend/core/src/components/attachment-viewer/pdf-viewer-inner.tsx b/packages/frontend/core/src/components/attachment-viewer/pdf-viewer-inner.tsx
new file mode 100644
index 0000000000000..ac2eac56b1480
--- /dev/null
+++ b/packages/frontend/core/src/components/attachment-viewer/pdf-viewer-inner.tsx
@@ -0,0 +1,197 @@
+import { IconButton, observeResize } from '@affine/component';
+import type {
+ PDF,
+ PDFRendererState,
+ PDFStatus,
+} from '@affine/core/modules/pdf';
+import {
+ Item,
+ List,
+ ListPadding,
+ ListWithSmallGap,
+ PDFPageRenderer,
+ type PDFVirtuosoContext,
+ type PDFVirtuosoProps,
+ Scroller,
+ ScrollSeekPlaceholder,
+} from '@affine/core/modules/pdf/views';
+import { CollapseIcon, ExpandIcon } from '@blocksuite/icons/rc';
+import clsx from 'clsx';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import {
+ type ScrollSeekConfiguration,
+ Virtuoso,
+ type VirtuosoHandle,
+} from 'react-virtuoso';
+
+import * as styles from './styles.css';
+import { calculatePageNum } from './utils';
+
+const THUMBNAIL_WIDTH = 94;
+
+export interface PDFViewerInnerProps {
+ pdf: PDF;
+ state: Extract;
+}
+
+export const PDFViewerInner = ({ pdf, state }: PDFViewerInnerProps) => {
+ const [cursor, setCursor] = useState(0);
+ const [collapsed, setCollapsed] = useState(true);
+ const [viewportInfo, setViewportInfo] = useState({ width: 0, height: 0 });
+
+ const viewerRef = useRef(null);
+ const pagesScrollerRef = useRef(null);
+ const pagesScrollerHandleRef = useRef(null);
+ const thumbnailsScrollerHandleRef = useRef(null);
+
+ const updateScrollerRef = useCallback(
+ (scroller: HTMLElement | Window | null) => {
+ pagesScrollerRef.current = scroller as HTMLElement;
+ },
+ []
+ );
+
+ const onScroll = useCallback(() => {
+ const el = pagesScrollerRef.current;
+ if (!el) return;
+
+ const { pageCount } = state.meta;
+ if (!pageCount) return;
+
+ const cursor = calculatePageNum(el, pageCount);
+
+ setCursor(cursor);
+ }, [pagesScrollerRef, state]);
+
+ const onPageSelect = useCallback(
+ (index: number) => {
+ const scroller = pagesScrollerHandleRef.current;
+ if (!scroller) return;
+
+ scroller.scrollToIndex({
+ index,
+ align: 'center',
+ behavior: 'smooth',
+ });
+ },
+ [pagesScrollerHandleRef]
+ );
+
+ const pageContent = useCallback(
+ (
+ index: number,
+ _: unknown,
+ { width, height, onPageSelect, pageClassName }: PDFVirtuosoContext
+ ) => {
+ return (
+
+ );
+ },
+ [pdf]
+ );
+
+ const thumbnailsConfig = useMemo(() => {
+ const { height: vh } = viewportInfo;
+ const { pageCount: t, height: h, width: w } = state.meta;
+ const p = h / (w || 1);
+ const pw = THUMBNAIL_WIDTH;
+ const ph = Math.ceil(pw * p);
+ const height = Math.min(vh - 60 - 24 - 24 - 2 - 8, t * ph + (t - 1) * 12);
+ return {
+ context: {
+ width: pw,
+ height: ph,
+ onPageSelect,
+ pageClassName: styles.pdfThumbnail,
+ },
+ style: { height },
+ };
+ }, [state, viewportInfo, onPageSelect]);
+
+ const scrollSeekConfig = useMemo(() => {
+ return {
+ enter: velocity => Math.abs(velocity) > 1024,
+ exit: velocity => Math.abs(velocity) < 10,
+ };
+ }, []);
+
+ useEffect(() => {
+ const viewer = viewerRef.current;
+ if (!viewer) return;
+ return observeResize(viewer, ({ contentRect: { width, height } }) =>
+ setViewportInfo({ width, height })
+ );
+ }, []);
+
+ return (
+
+
+ key={pdf.id}
+ ref={pagesScrollerHandleRef}
+ scrollerRef={updateScrollerRef}
+ onScroll={onScroll}
+ className={styles.virtuoso}
+ totalCount={state.meta.pageCount}
+ itemContent={pageContent}
+ components={{
+ Item,
+ List,
+ Scroller,
+ Header: ListPadding,
+ Footer: ListPadding,
+ ScrollSeekPlaceholder,
+ }}
+ context={{
+ width: state.meta.width,
+ height: state.meta.height,
+ pageClassName: styles.pdfPage,
+ }}
+ scrollSeekConfiguration={scrollSeekConfig}
+ />
+
+
+
+ key={`${pdf.id}-thumbnail`}
+ ref={thumbnailsScrollerHandleRef}
+ className={styles.virtuoso}
+ totalCount={state.meta.pageCount}
+ itemContent={pageContent}
+ components={{
+ Item,
+ List: ListWithSmallGap,
+ Scroller,
+ ScrollSeekPlaceholder,
+ }}
+ scrollSeekConfiguration={scrollSeekConfig}
+ style={thumbnailsConfig.style}
+ context={thumbnailsConfig.context}
+ />
+
+
+
+
+ {state.meta.pageCount > 0 ? cursor + 1 : 0}
+
+ /{state.meta.pageCount}
+
+
:
}
+ onClick={() => setCollapsed(!collapsed)}
+ />
+
+
+
+ );
+};
diff --git a/packages/frontend/core/src/components/attachment-viewer/pdf-viewer.tsx b/packages/frontend/core/src/components/attachment-viewer/pdf-viewer.tsx
index ceeffbf75f3c5..bd6719dc17cdd 100644
--- a/packages/frontend/core/src/components/attachment-viewer/pdf-viewer.tsx
+++ b/packages/frontend/core/src/components/attachment-viewer/pdf-viewer.tsx
@@ -1,206 +1,13 @@
-import { IconButton, observeResize } from '@affine/component';
-import {
- type PDF,
- type PDFRendererState,
- PDFService,
- PDFStatus,
-} from '@affine/core/modules/pdf';
-import {
- Item,
- List,
- ListPadding,
- ListWithSmallGap,
- LoadingSvg,
- PDFPageRenderer,
- type PDFVirtuosoContext,
- type PDFVirtuosoProps,
- Scroller,
- ScrollSeekPlaceholder,
-} from '@affine/core/modules/pdf/views';
import track from '@affine/track';
+import { type PDF, PDFService, PDFStatus } from '@affine/core/modules/pdf';
+import { LoadingSvg } from '@affine/core/modules/pdf/views';
import type { AttachmentBlockModel } from '@blocksuite/affine/blocks';
-import { CollapseIcon, ExpandIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
-import clsx from 'clsx';
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import {
- type ScrollSeekConfiguration,
- Virtuoso,
- type VirtuosoHandle,
-} from 'react-virtuoso';
+import { useEffect, useState } from 'react';
-import * as styles from './styles.css';
-import { calculatePageNum } from './utils';
+import { PDFViewerInner } from './pdf-viewer-inner';
-const THUMBNAIL_WIDTH = 94;
-
-interface ViewerProps {
- model: AttachmentBlockModel;
-}
-
-interface PDFViewerInnerProps {
- pdf: PDF;
- state: Extract;
-}
-
-const PDFViewerInner = ({ pdf, state }: PDFViewerInnerProps) => {
- const [cursor, setCursor] = useState(0);
- const [collapsed, setCollapsed] = useState(true);
- const [viewportInfo, setViewportInfo] = useState({ width: 0, height: 0 });
-
- const viewerRef = useRef(null);
- const pagesScrollerRef = useRef(null);
- const pagesScrollerHandleRef = useRef(null);
- const thumbnailsScrollerHandleRef = useRef(null);
-
- const onScroll = useCallback(() => {
- const el = pagesScrollerRef.current;
- if (!el) return;
-
- const { pageCount } = state.meta;
- if (!pageCount) return;
-
- const cursor = calculatePageNum(el, pageCount);
-
- setCursor(cursor);
- }, [pagesScrollerRef, state]);
-
- const onPageSelect = useCallback(
- (index: number) => {
- const scroller = pagesScrollerHandleRef.current;
- if (!scroller) return;
-
- scroller.scrollToIndex({
- index,
- align: 'center',
- behavior: 'smooth',
- });
- },
- [pagesScrollerHandleRef]
- );
-
- const pageContent = useCallback(
- (
- index: number,
- _: unknown,
- { width, height, onPageSelect, pageClassName }: PDFVirtuosoContext
- ) => {
- return (
-
- );
- },
- [pdf]
- );
-
- const thumbnailsConfig = useMemo(() => {
- const { height: vh } = viewportInfo;
- const { pageCount: t, height: h, width: w } = state.meta;
- const p = h / (w || 1);
- const pw = THUMBNAIL_WIDTH;
- const ph = Math.ceil(pw * p);
- const height = Math.min(vh - 60 - 24 - 24 - 2 - 8, t * ph + (t - 1) * 12);
- return {
- context: {
- width: pw,
- height: ph,
- onPageSelect,
- pageClassName: styles.pdfThumbnail,
- },
- style: { height },
- };
- }, [state, viewportInfo, onPageSelect]);
-
- const scrollSeekConfig = useMemo(() => {
- return {
- enter: velocity => Math.abs(velocity) > 1024,
- exit: velocity => Math.abs(velocity) < 10,
- };
- }, []);
-
- useEffect(() => {
- const viewer = viewerRef.current;
- if (!viewer) return;
- return observeResize(viewer, ({ contentRect: { width, height } }) =>
- setViewportInfo({ width, height })
- );
- }, []);
-
- return (
-
-
- key={pdf.id}
- ref={pagesScrollerHandleRef}
- scrollerRef={scroller => {
- pagesScrollerRef.current = scroller as HTMLElement;
- }}
- onScroll={onScroll}
- className={styles.virtuoso}
- totalCount={state.meta.pageCount}
- itemContent={pageContent}
- components={{
- Item,
- List,
- Scroller,
- Header: ListPadding,
- Footer: ListPadding,
- ScrollSeekPlaceholder,
- }}
- context={{
- width: state.meta.width,
- height: state.meta.height,
- pageClassName: styles.pdfPage,
- }}
- scrollSeekConfiguration={scrollSeekConfig}
- />
-
-
-
- key={`${pdf.id}-thumbnail`}
- ref={thumbnailsScrollerHandleRef}
- className={styles.virtuoso}
- totalCount={state.meta.pageCount}
- itemContent={pageContent}
- components={{
- Item,
- List: ListWithSmallGap,
- Scroller,
- ScrollSeekPlaceholder,
- }}
- scrollSeekConfiguration={scrollSeekConfig}
- style={thumbnailsConfig.style}
- context={thumbnailsConfig.context}
- />
-
-
-
-
- {state.meta.pageCount > 0 ? cursor + 1 : 0}
-
- /{state.meta.pageCount}
-
-
:
}
- onClick={() => setCollapsed(!collapsed)}
- />
-
-
-
- );
-};
-
-function PDFViewerStatus({ pdf }: { pdf: PDF }) {
+function PDFViewerStatus({ pdf, ...props }: PDFViewerProps & { pdf: PDF }) {
const state = useLiveData(pdf.state$);
useEffect(() => {
@@ -213,10 +20,17 @@ function PDFViewerStatus({ pdf }: { pdf: PDF }) {
return ;
}
- return ;
+ return ;
+}
+
+export interface PDFViewerProps {
+ model: AttachmentBlockModel;
+ name: string;
+ ext: string;
+ size: string;
}
-export function PDFViewer({ model }: ViewerProps) {
+export function PDFViewer({ model, ...props }: PDFViewerProps) {
const pdfService = useService(PDFService);
const [pdf, setPdf] = useState(null);
@@ -231,5 +45,5 @@ export function PDFViewer({ model }: ViewerProps) {
return ;
}
- return ;
+ return ;
}
diff --git a/packages/frontend/core/src/components/attachment-viewer/styles.css.ts b/packages/frontend/core/src/components/attachment-viewer/styles.css.ts
index b1abeaa6c05d2..1e85631d4e6c5 100644
--- a/packages/frontend/core/src/components/attachment-viewer/styles.css.ts
+++ b/packages/frontend/core/src/components/attachment-viewer/styles.css.ts
@@ -49,6 +49,10 @@ export const titlebarName = style({
wordWrap: 'break-word',
});
+export const virtuoso = style({
+ width: '100%',
+});
+
export const error = style({
flexDirection: 'column',
alignItems: 'center',
@@ -106,10 +110,6 @@ export const viewer = style({
},
});
-export const virtuoso = style({
- width: '100%',
-});
-
export const pdfIndicator = style({
display: 'flex',
alignItems: 'center',
@@ -127,6 +127,7 @@ export const pdfPage = style({
boxShadow:
'0px 4px 20px 0px var(--transparent-black-200, rgba(0, 0, 0, 0.10))',
overflow: 'hidden',
+ maxHeight: 'max-content',
});
export const pdfThumbnails = style({
@@ -156,7 +157,6 @@ export const pdfThumbnailsList = style({
flexDirection: 'column',
maxHeight: '100%',
overflow: 'hidden',
- resize: 'both',
selectors: {
'&.collapsed': {
display: 'none',
diff --git a/packages/frontend/core/src/components/attachment-viewer/styles.embedded.css.ts b/packages/frontend/core/src/components/attachment-viewer/styles.embedded.css.ts
new file mode 100644
index 0000000000000..06d2332c33fdf
--- /dev/null
+++ b/packages/frontend/core/src/components/attachment-viewer/styles.embedded.css.ts
@@ -0,0 +1,88 @@
+import { cssVar } from '@toeverything/theme';
+import { cssVarV2 } from '@toeverything/theme/v2';
+import { style } from '@vanilla-extract/css';
+
+export const pdfContainer = style({
+ width: '100%',
+ borderRadius: '8px',
+ borderWidth: '1px',
+ borderStyle: 'solid',
+ borderColor: cssVarV2('layer/insideBorder/border'),
+ background: cssVar('--affine-background-primary-color'),
+ userSelect: 'none',
+ contentVisibility: 'visible',
+});
+
+export const pdfViewer = style({
+ position: 'relative',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: '12px',
+ overflow: 'hidden',
+ background: cssVarV2('layer/background/secondary'),
+});
+
+export const pdfPlaceholder = style({
+ position: 'absolute',
+ maxWidth: 'calc(100% - 24px)',
+ overflow: 'hidden',
+ height: 'auto',
+ pointerEvents: 'none',
+});
+
+export const pdfControls = style({
+ position: 'absolute',
+ bottom: '16px',
+ right: '14px',
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '10px',
+});
+
+export const pdfControlButton = style({
+ width: '36px',
+ height: '36px',
+ borderWidth: '1px',
+ borderStyle: 'solid',
+ borderColor: cssVar('--affine-border-color'),
+ background: cssVar('--affine-white'),
+});
+
+export const pdfFooter = style({
+ display: 'flex',
+ alignItems: 'center',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ gap: '12px',
+ padding: '12px',
+ textWrap: 'nowrap',
+});
+
+export const pdfFooterItem = style({
+ display: 'flex',
+ alignItems: 'center',
+ selectors: {
+ '&.truncate': {
+ overflow: 'hidden',
+ },
+ },
+});
+
+export const pdfTitle = style({
+ marginLeft: '8px',
+ fontSize: '14px',
+ fontWeight: 600,
+ lineHeight: '22px',
+ color: cssVarV2('text/primary'),
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap',
+});
+
+export const pdfPageCount = style({
+ fontSize: '12px',
+ fontWeight: 400,
+ lineHeight: '20px',
+ color: cssVarV2('text/secondary'),
+});
diff --git a/packages/frontend/core/src/components/attachment-viewer/utils.ts b/packages/frontend/core/src/components/attachment-viewer/utils.ts
index fb0e5c8f8383b..b2cb707fef6a5 100644
--- a/packages/frontend/core/src/components/attachment-viewer/utils.ts
+++ b/packages/frontend/core/src/components/attachment-viewer/utils.ts
@@ -2,6 +2,7 @@ import type { AttachmentBlockModel } from '@blocksuite/affine/blocks';
import { filesize } from 'filesize';
import { downloadBlob } from '../../utils/resource';
+import type { PDFViewerProps } from './pdf-viewer';
export async function getAttachmentBlob(model: AttachmentBlockModel) {
const sourceId = model.sourceId;
@@ -26,7 +27,9 @@ export async function download(model: AttachmentBlockModel) {
await downloadBlob(blob, model.name);
}
-export function buildAttachmentProps(model: AttachmentBlockModel) {
+export function buildAttachmentProps(
+ model: AttachmentBlockModel
+): PDFViewerProps {
const pieces = model.name.split('.');
const ext = pieces.pop() || '';
const name = pieces.join('.');
diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx
index 5857f08601b4b..e4abc73a81d2a 100644
--- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx
+++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx
@@ -53,6 +53,7 @@ import {
patchDocModeService,
patchEdgelessClipboard,
patchEmbedLinkedDocBlockConfig,
+ patchForAttachmentEmbedViews,
patchForMobile,
patchForSharedPage,
patchGenerateDocUrlExtension,
@@ -148,6 +149,7 @@ const usePatchSpecs = (shared: boolean, mode: DocMode) => {
let patched = specs.concat(
patchReferenceRenderer(reactToLit, referenceRenderer)
);
+ patched = patched.concat(patchForAttachmentEmbedViews(reactToLit));
patched = patched.concat(patchNotificationService(confirmModal));
patched = patched.concat(patchPeekViewService(peekViewService));
patched = patched.concat(patchEdgelessClipboard());
diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/attachment-block.ts b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/attachment-block.ts
index 128cc458b4c1a..b08da3c3e96b9 100644
--- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/attachment-block.ts
+++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/attachment-block.ts
@@ -1,7 +1,7 @@
+import type { ExtensionType } from '@blocksuite/affine/block-std';
import {
BlockFlavourIdentifier,
BlockServiceIdentifier,
- type ExtensionType,
StdIdentifier,
} from '@blocksuite/affine/block-std';
import {
diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx
index 374f633b3bb4b..0b2af7822ce54 100644
--- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx
+++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx
@@ -7,6 +7,9 @@ import {
toReactNode,
type useConfirmModal,
} from '@affine/component';
+import { AttachmentPreviewErrorBoundary } from '@affine/core/components/attachment-viewer/error';
+import { PDFViewerEmbedded } from '@affine/core/components/attachment-viewer/pdf-viewer-embedded';
+import { buildAttachmentProps } from '@affine/core/components/attachment-viewer/utils';
import { WorkspaceServerService } from '@affine/core/modules/cloud';
import { type DocService, DocsService } from '@affine/core/modules/doc';
import type { EditorService } from '@affine/core/modules/editor';
@@ -47,6 +50,7 @@ import type {
} from '@blocksuite/affine/blocks';
import {
AffineSlashMenuWidget,
+ AttachmentEmbedConfigIdentifier,
DocModeExtension,
EdgelessRootBlockComponent,
EmbedLinkedDocBlockComponent,
@@ -59,6 +63,7 @@ import {
QuickSearchExtension,
ReferenceNodeConfigExtension,
} from '@blocksuite/affine/blocks';
+import { Bound } from '@blocksuite/affine/global/utils';
import { type BlockSnapshot, Text } from '@blocksuite/affine/store';
import type { ReferenceParams } from '@blocksuite/affine-model';
import {
@@ -597,3 +602,33 @@ export function patchForMobile() {
];
return extensions;
}
+
+export function patchForAttachmentEmbedViews(
+ reactToLit: (element: ElementOrFactory) => TemplateResult
+): ExtensionType {
+ return {
+ setup: di => {
+ di.override(AttachmentEmbedConfigIdentifier('pdf'), () => ({
+ name: 'pdf',
+ check: (model, maxFileSize) =>
+ model.type === 'application/pdf' && model.size <= maxFileSize,
+ action: model => {
+ const bound = Bound.deserialize(model.xywh);
+ bound.w = 537 + 24 + 2;
+ bound.h = 759 + 46 + 24 + 2;
+ model.doc.updateBlock(model, {
+ embed: true,
+ style: 'pdf',
+ xywh: bound.serialize(),
+ });
+ },
+ template: (model, _blobUrl) =>
+ reactToLit(
+
+
+
+ ),
+ }));
+ },
+ };
+}
diff --git a/packages/frontend/core/src/modules/pdf/renderer/worker.ts b/packages/frontend/core/src/modules/pdf/renderer/worker.ts
index b435c8006ea03..cd5f9351f940b 100644
--- a/packages/frontend/core/src/modules/pdf/renderer/worker.ts
+++ b/packages/frontend/core/src/modules/pdf/renderer/worker.ts
@@ -103,8 +103,9 @@ class PDFRendererBackend extends OpConsumer {
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 {
);
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();
diff --git a/packages/frontend/core/src/modules/pdf/views/components.tsx b/packages/frontend/core/src/modules/pdf/views/components.tsx
index 05c5fa87f6a33..c7af3e29ca89b 100644
--- a/packages/frontend/core/src/modules/pdf/views/components.tsx
+++ b/packages/frontend/core/src/modules/pdf/views/components.tsx
@@ -113,3 +113,9 @@ export const LoadingSvg = memo(
);
LoadingSvg.displayName = 'pdf-loading';
+
+export const PDFPageCanvas = forwardRef((props, ref) => {
+ return ;
+});
+
+PDFPageCanvas.displayName = 'pdf-page-canvas';
diff --git a/packages/frontend/core/src/modules/pdf/views/index.ts b/packages/frontend/core/src/modules/pdf/views/index.ts
index 7314709cc2d3b..2395d2230d1a6 100644
--- a/packages/frontend/core/src/modules/pdf/views/index.ts
+++ b/packages/frontend/core/src/modules/pdf/views/index.ts
@@ -4,6 +4,7 @@ export {
ListPadding,
ListWithSmallGap,
LoadingSvg,
+ PDFPageCanvas,
type PDFVirtuosoContext,
type PDFVirtuosoProps,
Scroller,
diff --git a/packages/frontend/core/src/modules/pdf/views/page-renderer.tsx b/packages/frontend/core/src/modules/pdf/views/page-renderer.tsx
index 80a3dcb6bb840..e9ad029df53f1 100644
--- a/packages/frontend/core/src/modules/pdf/views/page-renderer.tsx
+++ b/packages/frontend/core/src/modules/pdf/views/page-renderer.tsx
@@ -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 ? (
-
- ) : (
-
- )}
+ {img === null ? : }
);
};
diff --git a/packages/frontend/core/src/modules/pdf/views/styles.css.ts b/packages/frontend/core/src/modules/pdf/views/styles.css.ts
index 0b45c32164b15..6584895a85514 100644
--- a/packages/frontend/core/src/modules/pdf/views/styles.css.ts
+++ b/packages/frontend/core/src/modules/pdf/views/styles.css.ts
@@ -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',
});
diff --git a/packages/frontend/core/src/modules/peek-view/entities/peek-view.ts b/packages/frontend/core/src/modules/peek-view/entities/peek-view.ts
index c8ac94f6a0c72..26aaa870e6803 100644
--- a/packages/frontend/core/src/modules/peek-view/entities/peek-view.ts
+++ b/packages/frontend/core/src/modules/peek-view/entities/peek-view.ts
@@ -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: {