- {state.meta.pageCount > 0 ? cursor + 1 : 0}
+ {meta.pageCount > 0 ? cursor + 1 : 0}
- /{state.meta.pageCount}
+ /{meta.pageCount}
:
}
@@ -246,11 +259,76 @@ export const PDFViewerInner = ({ pdf, state }: PDFViewerInnerProps) => {
);
};
-function PDFViewerStatus({
- pdf,
+type PDFViewerStatusProps = {
+ message: string;
+ reload: () => void;
+};
+
+function PDFViewerStatusMenuItems({ message, reload }: PDFViewerStatusProps) {
+ const onClick = useCallback(
+ (e: MouseEvent) => {
+ e.stopPropagation();
+
+ reload();
+ },
+ [reload]
+ );
+
+ return (
+
+
{message}
+
+
+
+
+ );
+}
+
+function PDFViewerStatus(props: PDFViewerStatusProps) {
+ return (
+
+
}
+ contentWrapperStyle={{
+ padding: '8px',
+ boxShadow: cssVar('overlayShadow'),
+ }}
+ contentOptions={{
+ sideOffset: 8,
+ }}
+ >
+
+
+
+ );
+}
+
+function PDFViewerContainer({
+ model,
+ reload,
...props
-}: AttachmentViewerProps & { pdf: PDF }) {
- const state = useLiveData(pdf.state$);
+}: AttachmentViewerProps & { reload: () => void }) {
+ const pdfService = useService(PDFService);
+ const [pdf, setPdf] = useState
(null);
+ const state = useLiveData(
+ useMemo(
+ () =>
+ pdf?.state$ ??
+ new LiveData({ status: PDFStatus.IDLE }),
+ [pdf]
+ )
+ );
useEffect(() => {
if (state.status !== PDFStatus.Error) return;
@@ -258,17 +336,6 @@ function PDFViewerStatus({
track.$.attachment.$.openPDFRendererFail();
}, [state]);
- if (state?.status !== PDFStatus.Opened) {
- return ;
- }
-
- return ;
-}
-
-export function PDFViewer({ model, ...props }: AttachmentViewerProps) {
- const pdfService = useService(PDFService);
- const [pdf, setPdf] = useState(null);
-
useEffect(() => {
const { pdf, release } = pdfService.get(model);
setPdf(pdf);
@@ -278,15 +345,28 @@ export function PDFViewer({ model, ...props }: AttachmentViewerProps) {
};
}, [model, pdfService, setPdf]);
- if (!pdf) {
- return ;
+ if (pdf && state.status === PDFStatus.Opened) {
+ return ;
}
- return ;
+ return (
+ <>
+
+ {state.status === PDFStatus.Error && (
+
+ )}
+ >
+ );
}
const PDFLoading = () => (
-
+
);
+
+export function PDFViewer(props: AttachmentViewerProps) {
+ const [refreshKey, setRefreshKey] = useState
(null);
+ const reload = useCallback(() => setRefreshKey(nanoid()), []);
+ return ;
+}
diff --git a/packages/frontend/core/src/blocksuite/attachment-viewer/pdf/styles.css.ts b/packages/frontend/core/src/blocksuite/attachment-viewer/pdf/styles.css.ts
index 90c306de51..a11bd7ef2b 100644
--- a/packages/frontend/core/src/blocksuite/attachment-viewer/pdf/styles.css.ts
+++ b/packages/frontend/core/src/blocksuite/attachment-viewer/pdf/styles.css.ts
@@ -89,7 +89,7 @@ export const pdfContainer = style({
borderWidth: '1px',
borderStyle: 'solid',
borderColor: cssVarV2('layer/insideBorder/border'),
- background: cssVar('--affine-background-primary-color'),
+ background: cssVar('backgroundPrimaryColor'),
userSelect: 'none',
contentVisibility: 'visible',
display: 'flex',
@@ -132,8 +132,8 @@ export const pdfControlButton = style({
height: '36px',
borderWidth: '1px',
borderStyle: 'solid',
- borderColor: cssVar('--affine-border-color'),
- background: cssVar('--affine-white'),
+ borderColor: cssVar('borderColor'),
+ background: cssVar('white'),
});
export const pdfFooter = style({
@@ -173,3 +173,53 @@ export const pdfPageCount = style({
lineHeight: '20px',
color: cssVarV2('text/secondary'),
});
+
+export const pdfLoadingWrapper = style({
+ margin: 'auto',
+});
+
+export const pdfStatus = style({
+ position: 'absolute',
+ left: '18px',
+ bottom: '18px',
+});
+
+export const pdfStatusButton = style({
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: '24px',
+ height: '24px',
+ borderRadius: '50%',
+ fontSize: '18px',
+ outline: 'none',
+ border: 'none',
+ cursor: 'pointer',
+ color: cssVarV2('button/pureWhiteText'),
+ background: cssVarV2('status/error'),
+ boxShadow: cssVar('overlayShadow'),
+});
+
+export const pdfStatusMenu = style({
+ width: '244px',
+ gap: '8px',
+ color: cssVarV2('text/primary'),
+ lineHeight: '22px',
+});
+
+export const pdfStatusMenuFooter = style({
+ display: 'flex',
+ justifyContent: 'flex-end',
+});
+
+export const pdfReloadButton = style({
+ display: 'flex',
+ alignItems: 'center',
+ padding: '2px 12px',
+ borderRadius: '8px',
+ border: 'none',
+ background: 'none',
+ cursor: 'pointer',
+ outline: 'none',
+ color: cssVarV2('button/primary'),
+});
diff --git a/packages/frontend/core/src/desktop/pages/workspace/attachment/index.tsx b/packages/frontend/core/src/desktop/pages/workspace/attachment/index.tsx
index 843cff6e92..d6c9ecf399 100644
--- a/packages/frontend/core/src/desktop/pages/workspace/attachment/index.tsx
+++ b/packages/frontend/core/src/desktop/pages/workspace/attachment/index.tsx
@@ -16,6 +16,7 @@ type AttachmentPageProps = {
};
const useLoadAttachment = (pageId: string, attachmentId: string) => {
+ const [loading, setLoading] = useState(true);
const docsService = useService(DocsService);
const docRecord = useLiveData(docsService.list.doc$(pageId));
const [doc, setDoc] = useState(null);
@@ -23,6 +24,7 @@ const useLoadAttachment = (pageId: string, attachmentId: string) => {
useLayoutEffect(() => {
if (!docRecord) {
+ setLoading(false);
return;
}
@@ -38,44 +40,23 @@ const useLoadAttachment = (pageId: string, attachmentId: string) => {
doc
.waitForSyncReady()
.then(() => {
- const block = doc.blockSuiteDoc.getBlock(attachmentId);
- if (block) {
- setModel(block.model as AttachmentBlockModel);
- }
+ const model =
+ doc.blockSuiteDoc.getModelById(attachmentId);
+ setModel(model);
})
- .catch(console.error);
+ .catch(console.error)
+ .finally(() => setLoading(false));
return () => {
release();
dispose();
};
- }, [docRecord, docsService, pageId, attachmentId]);
+ }, [docRecord, docsService, pageId, attachmentId, setLoading]);
- return { doc, model };
+ return { doc, model, loading };
};
-export const AttachmentPage = ({
- pageId,
- attachmentId,
-}: AttachmentPageProps): ReactElement => {
- const { doc, model } = useLoadAttachment(pageId, attachmentId);
-
- if (!doc) {
- return ;
- }
-
- if (doc && model) {
- return (
-
-
-
-
-
- );
- }
-
+const Loading = () => {
return (
{
+ const { doc, model, loading } = useLoadAttachment(pageId, attachmentId);
+
+ if (loading) {
+ return ;
+ }
+
+ if (doc && model) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return ;
+};
+
export const Component = () => {
const { pageId, attachmentId } = useParams();
- if (!pageId || !attachmentId) {
- return ;
+ if (pageId && attachmentId) {
+ return ;
}
- return ;
+ return ;
};
diff --git a/packages/frontend/core/src/modules/media/utils.ts b/packages/frontend/core/src/modules/media/utils.ts
index 3a2a567d58..24b973562d 100644
--- a/packages/frontend/core/src/modules/media/utils.ts
+++ b/packages/frontend/core/src/modules/media/utils.ts
@@ -1,49 +1,62 @@
import type { AttachmentBlockModel } from '@blocksuite/affine/model';
+const imageExts = new Set([
+ 'jpg',
+ 'jpeg',
+ 'png',
+ 'gif',
+ 'webp',
+ 'svg',
+ 'avif',
+ 'tiff',
+ 'bmp',
+]);
+
+const audioExts = new Set(['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac', 'opus']);
+
+const videoExts = new Set([
+ 'mp4',
+ 'webm',
+ 'avi',
+ 'mov',
+ 'mkv',
+ 'mpeg',
+ 'ogv',
+ '3gp',
+]);
+
export function getAttachmentType(model: AttachmentBlockModel) {
+ const type = model.props.type;
+
// Check MIME type first
- if (model.props.type.startsWith('image/')) {
+ if (type.startsWith('image/')) {
return 'image';
}
- if (model.props.type.startsWith('audio/')) {
+ if (type.startsWith('audio/')) {
return 'audio';
}
- if (model.props.type.startsWith('video/')) {
+ if (type.startsWith('video/')) {
return 'video';
}
- if (model.props.type === 'application/pdf') {
+ if (type === 'application/pdf') {
return 'pdf';
}
// If MIME type doesn't match, check file extension
- const ext = model.props.name.split('.').pop()?.toLowerCase() || '';
+ const ext = model.props.name.split('.').pop()?.toLowerCase() ?? '';
- if (
- [
- 'jpg',
- 'jpeg',
- 'png',
- 'gif',
- 'webp',
- 'svg',
- 'avif',
- 'tiff',
- 'bmp',
- ].includes(ext)
- ) {
+ if (imageExts.has(ext)) {
return 'image';
}
- if (['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac', 'opus'].includes(ext)) {
+ if (audioExts.has(ext)) {
return 'audio';
}
- if (
- ['mp4', 'webm', 'avi', 'mov', 'mkv', 'mpeg', 'ogv', '3gp'].includes(ext)
- ) {
+ if (videoExts.has(ext)) {
return 'video';
}
@@ -55,7 +68,7 @@ export function getAttachmentType(model: AttachmentBlockModel) {
}
export async function downloadBlobToBuffer(model: AttachmentBlockModel) {
- const sourceId = model.props.sourceId;
+ const sourceId = model.props.sourceId$.peek();
if (!sourceId) {
throw new Error('Attachment not found');
}
@@ -65,6 +78,5 @@ export async function downloadBlobToBuffer(model: AttachmentBlockModel) {
throw new Error('Attachment not found');
}
- const arrayBuffer = await blob.arrayBuffer();
- return arrayBuffer;
+ return await blob.arrayBuffer();
}
diff --git a/packages/frontend/core/src/modules/pdf/entities/pdf.ts b/packages/frontend/core/src/modules/pdf/entities/pdf.ts
index b874110f6c..3e2306af46 100644
--- a/packages/frontend/core/src/modules/pdf/entities/pdf.ts
+++ b/packages/frontend/core/src/modules/pdf/entities/pdf.ts
@@ -37,9 +37,7 @@ export class PDF extends Entity {
readonly state$ = LiveData.from(
// @ts-expect-error type alias
from(downloadBlobToBuffer(this.props)).pipe(
- switchMap(buffer => {
- return this.renderer.ob$('open', { data: buffer });
- }),
+ switchMap(data => this.renderer.ob$('open', { data })),
map(meta => ({ status: PDFStatus.Opened, meta })),
// @ts-expect-error type alias
startWith({ status: PDFStatus.Opening }),
diff --git a/packages/frontend/core/src/modules/pdf/index.ts b/packages/frontend/core/src/modules/pdf/index.ts
index 5a6a4f5a15..fa64c51bfc 100644
--- a/packages/frontend/core/src/modules/pdf/index.ts
+++ b/packages/frontend/core/src/modules/pdf/index.ts
@@ -15,5 +15,5 @@ export function configurePDFModule(framework: Framework) {
export { PDF, type PDFRendererState, PDFStatus } from './entities/pdf';
export { PDFPage } from './entities/pdf-page';
-export { PDFRenderer } from './renderer';
+export { type PDFMeta, PDFRenderer } from './renderer';
export { PDFService } from './services/pdf';
diff --git a/packages/frontend/core/src/modules/peek-view/view/attachment-preview/index.tsx b/packages/frontend/core/src/modules/peek-view/view/attachment-preview/index.tsx
index 2b970edef1..98f3785e8f 100644
--- a/packages/frontend/core/src/modules/peek-view/view/attachment-preview/index.tsx
+++ b/packages/frontend/core/src/modules/peek-view/view/attachment-preview/index.tsx
@@ -15,11 +15,14 @@ export const AttachmentPreviewPeekView = ({
}: AttachmentPreviewModalProps) => {
const { doc } = useEditor(docId);
const blocksuiteDoc = doc?.blockSuiteDoc;
- const model = useMemo(() => {
- const model = blocksuiteDoc?.getBlock(blockId)?.model;
- if (!model) return null;
- return model as AttachmentBlockModel;
- }, [blockId, blocksuiteDoc]);
+ const model = useMemo(
+ () => blocksuiteDoc?.getModelById(blockId) ?? null,
+ [blockId, blocksuiteDoc]
+ );
- return model === null ? null : ;
+ if (model) {
+ return ;
+ }
+
+ return null;
};
diff --git a/tests/affine-local/e2e/attachment-preview.spec.ts b/tests/affine-local/e2e/attachment-preview.spec.ts
index ccf529dbf5..f6dc89a407 100644
--- a/tests/affine-local/e2e/attachment-preview.spec.ts
+++ b/tests/affine-local/e2e/attachment-preview.spec.ts
@@ -355,7 +355,6 @@ test('should re-render pdf viewer', async ({ page }) => {
const title = getBlockSuiteEditorTitle(page);
await title.click();
- await page.keyboard.type('PDF preview');
await page.keyboard.press('Enter');
@@ -386,3 +385,78 @@ test('should re-render pdf viewer', async ({ page }) => {
expect(portalId).not.toEqual(newPortalId);
});
+
+test('should display status when an error is thrown in peek view', async ({
+ context,
+ page,
+}) => {
+ await openHomePage(page);
+ await clickNewPageButton(page);
+ await waitForEmptyEditor(page);
+
+ const title = getBlockSuiteEditorTitle(page);
+ await title.click();
+
+ await page.keyboard.press('Enter');
+
+ await importAttachment(page, 'lorem-ipsum.pdf');
+
+ const attachment = page.locator('affine-attachment');
+ await attachment.click();
+
+ const toolbar = locateToolbar(page);
+
+ // Switches to embed view
+ await toolbar.getByLabel('Switch view').click();
+ await toolbar.getByLabel('Embed view').click();
+
+ await context.setOffline(true);
+
+ await attachment.dblclick();
+
+ // Peek view
+ const pdfViewer = page.getByTestId('pdf-viewer');
+ await expect(pdfViewer).toBeHidden();
+
+ const statusWrapper = page.getByTestId('pdf-viewer-status-wrapper');
+ await expect(statusWrapper).toBeVisible();
+});
+
+test('should display 404 when attachment is not found', async ({ page }) => {
+ await openHomePage(page);
+ await clickNewPageButton(page);
+ await waitForEmptyEditor(page);
+
+ const title = getBlockSuiteEditorTitle(page);
+ await title.click();
+
+ await page.keyboard.press('Enter');
+
+ await importAttachment(page, 'lorem-ipsum.pdf');
+
+ const attachment = page.locator('affine-attachment');
+ await attachment.click();
+
+ const toolbar = locateToolbar(page);
+
+ // Switches to embed view
+ await toolbar.getByLabel('Switch view').click();
+ await toolbar.getByLabel('Embed view').click();
+
+ await attachment.dblclick();
+
+ // Peek view
+ const pdfViewer = page.getByTestId('pdf-viewer');
+ await expect(pdfViewer).toBeVisible();
+
+ const statusWrapper = page.getByTestId('pdf-viewer-status-wrapper');
+ await expect(statusWrapper).toBeHidden();
+
+ await clickPeekViewControl(page, 1);
+
+ await page.goto(page.url() + 'test');
+
+ await expect(pdfViewer).toBeHidden();
+
+ await expect(page.getByTestId('not-found')).toBeVisible();
+});