feat(editor): improve status display in attachment embed view (#12180)

Closes: [BS-3438](https://linear.app/affine-design/issue/BS-3438/attachment-embed-view-中的-status-组件)
Closes: [BS-3447](https://linear.app/affine-design/issue/BS-3447/触发-litportal-re-render)

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

- **New Features**
  - Introduced a visual status indicator for embedded attachments with reload capability.
  - Added a new resource status component to display error messages and reload actions.
- **Improvements**
  - Enhanced attachment rendering flow with reactive state and unified embed handling.
  - Simplified resource state and blob URL lifecycle management.
  - Added status visibility flags for PDF and video embeds.
- **Bug Fixes**
  - Improved error handling and refresh support for embedded content including PDFs, videos, and audio.
- **Style**
  - Added styles for the attachment embed status indicator positioning.
- **Refactor**
  - Streamlined attachment and resource controller implementations for better maintainability.
- **Tests**
  - Added end-to-end test verifying PDF viewer reload and re-rendering in embed mode.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
fundon
2025-05-10 08:34:47 +00:00
parent 66500669c8
commit b3f0f38b41
11 changed files with 567 additions and 281 deletions
@@ -39,9 +39,22 @@ export type AttachmentEmbedConfig = {
std: BlockStdScope
) => Promise<void> | void;
/**
* The template will be used to render the embed view.
* Renders the embed view.
*/
template?: (model: AttachmentBlockModel, blobUrl: string) => TemplateResult;
render?: (
model: AttachmentBlockModel,
blobUrl: string
) => TemplateResult | null;
/**
* Should show status when turned on.
*/
shouldShowStatus?: boolean;
/**
* Should block type conversion be required.
*/
shouldBeConverted?: boolean;
};
// Single embed config.
@@ -115,26 +128,38 @@ export class AttachmentEmbedService extends Extension {
return this.values.some(config => config.check(model, maxFileSize));
}
render(
getRender(model: AttachmentBlockModel, maxFileSize = this._maxFileSize) {
return (
this.values.find(config => config.check(model, maxFileSize))?.render ??
null
);
}
shouldShowStatus(
model: AttachmentBlockModel,
blobUrl?: string,
maxFileSize = this._maxFileSize
) {
if (!model.props.embed || !blobUrl) return;
return (
this.values.find(config => config.check(model, maxFileSize))
?.shouldShowStatus ?? false
);
}
const config = this.values.find(config => config.check(model, maxFileSize));
if (!config || !config.template) {
console.error('No embed view template found!', model, model.props.type);
return;
}
return config.template(model, blobUrl);
shouldBeConverted(
model: AttachmentBlockModel,
maxFileSize = this._maxFileSize
) {
return (
this.values.find(config => config.check(model, maxFileSize))
?.shouldBeConverted ?? false
);
}
}
const embedConfig: AttachmentEmbedConfig[] = [
{
name: 'image',
shouldBeConverted: true,
check: model =>
model.store.schema.flavourSchemaMap.has('affine:image') &&
model.props.type.startsWith('image/'),
@@ -147,6 +172,7 @@ const embedConfig: AttachmentEmbedConfig[] = [
},
{
name: 'pdf',
shouldShowStatus: true,
check: (model, maxFileSize) =>
model.props.type === 'application/pdf' && model.props.size <= maxFileSize,
action: model => {
@@ -159,7 +185,7 @@ const embedConfig: AttachmentEmbedConfig[] = [
xywh: bound.serialize(),
});
},
template: (_, blobUrl) => {
render: (_, blobUrl) => {
// More options: https://tinytip.co/tips/html-pdf-params/
// https://chromium.googlesource.com/chromium/src/+/refs/tags/121.0.6153.1/chrome/browser/resources/pdf/open_pdf_params_parser.ts
const parameters = '#toolbar=0';
@@ -185,6 +211,7 @@ const embedConfig: AttachmentEmbedConfig[] = [
},
{
name: 'video',
shouldShowStatus: true,
check: (model, maxFileSize) =>
model.props.type.startsWith('video/') && model.props.size <= maxFileSize,
action: model => {
@@ -197,7 +224,7 @@ const embedConfig: AttachmentEmbedConfig[] = [
xywh: bound.serialize(),
});
},
template: (_, blobUrl) =>
render: (_, blobUrl) =>
html`<video
style=${styleMap({
display: 'flex',
@@ -216,8 +243,12 @@ const embedConfig: AttachmentEmbedConfig[] = [
name: 'audio',
check: (model, maxFileSize) =>
model.props.type.startsWith('audio/') && model.props.size <= maxFileSize,
template: (_, blobUrl) =>
html`<audio controls src=${blobUrl} style="margin: 4px;"></audio>`,
render: (_, blobUrl) =>
html`<audio
style=${styleMap({ margin: '4px' })}
src=${blobUrl}
controls
></audio>`,
},
];