feat(core): add status to pdf viewer (#12349)

Closes: [BS-3439](https://linear.app/affine-design/issue/BS-3439/pdf-独立页面split-view-中的-status-组件)
Related to: [BS-3143](https://linear.app/affine-design/issue/BS-3143/更新-loading-和错误样式)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added a dedicated error handling and reload interface for PDF attachments, allowing users to retry loading PDFs when errors occur.

- **Refactor**
  - Improved PDF viewer interface with clearer loading and error states.
  - Enhanced attachment type detection for better performance and maintainability.
  - Streamlined attachment preview logic for more direct and efficient model retrieval.
  - Simplified internal PDF metadata handling and control flow for improved clarity.
  - Clarified conditional rendering logic in attachment viewer components.
  - Introduced explicit loading state management and refined rendering logic in attachment pages.

- **Style**
  - Updated and added styles for PDF viewer controls and error status display.

- **Tests**
  - Added end-to-end tests validating PDF preview error handling and attachment not-found scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
fundon
2025-05-22 01:11:03 +00:00
parent 346c0df800
commit 21ea65edc5
10 changed files with 341 additions and 114 deletions
@@ -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();
}
@@ -37,9 +37,7 @@ export class PDF extends Entity<AttachmentBlockModel> {
readonly state$ = LiveData.from<PDFRendererState>(
// @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 }),
@@ -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';
@@ -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<AttachmentBlockModel>(blockId) ?? null,
[blockId, blocksuiteDoc]
);
return model === null ? null : <AttachmentViewer model={model} />;
if (model) {
return <AttachmentViewer model={model} />;
}
return null;
};