import type { BlockCaptionEditor } from '@blocksuite/affine-components/caption'; import { Peekable } from '@blocksuite/affine-components/peek'; import type { ImageBlockModel } from '@blocksuite/affine-model'; import { GfxBlockComponent } from '@blocksuite/block-std'; import { css, html } from 'lit'; import { property, query, state } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; import { when } from 'lit/directives/when.js'; import type { ImageBlockFallbackCard } from './components/image-block-fallback.js'; import type { ImageBlockService } from './image-service.js'; import { copyImageBlob, downloadImageBlob, fetchImageBlob, resetImageSize, turnImageIntoCardView, } from './utils.js'; @Peekable() export class ImageEdgelessBlockComponent extends GfxBlockComponent< ImageBlockModel, ImageBlockService > { static override styles = css` affine-edgeless-image .resizable-img, affine-edgeless-image .resizable-img img { width: 100%; height: 100%; } `; convertToCardView = () => { turnImageIntoCardView(this).catch(console.error); }; copy = () => { copyImageBlob(this).catch(console.error); }; download = () => { downloadImageBlob(this).catch(console.error); }; refreshData = () => { this.retryCount = 0; fetchImageBlob(this) .then(() => { const { width, height } = this.model; if (!width || !height) { return resetImageSize(this); } return; }) .catch(console.error); }; private _handleError(error: Error) { this.dispatchEvent(new CustomEvent('error', { detail: error })); } override connectedCallback() { super.connectedCallback(); this.refreshData(); this.contentEditable = 'false'; this.disposables.add( this.model.propsUpdated.on(({ key }) => { if (key === 'sourceId') { this.refreshData(); } }) ); } override disconnectedCallback() { if (this.blobUrl) { URL.revokeObjectURL(this.blobUrl); } super.disconnectedCallback(); } override renderGfxBlock() { const rotate = this.model.rotate ?? 0; const containerStyleMap = styleMap({ position: 'relative', width: '100%', transform: `rotate(${rotate}deg)`, transformOrigin: 'center', }); return html`
${when( this.loading || this.error || !this.blobUrl, () => html``, () => html`
` )}
${Object.values(this.widgets)} `; } override updated() { this.fallbackCard?.requestUpdate(); } @property({ attribute: false }) accessor blob: Blob | undefined = undefined; @property({ attribute: false }) accessor blobUrl: string | undefined = undefined; @query('block-caption-editor') accessor captionEditor!: BlockCaptionEditor | null; @property({ attribute: false }) accessor downloading = false; @property({ attribute: false }) accessor error = false; @query('affine-image-fallback-card') accessor fallbackCard: ImageBlockFallbackCard | null = null; @state() accessor lastSourceId!: string; @property({ attribute: false }) accessor loading = false; @query('.resizable-img') accessor resizableImg!: HTMLDivElement; @property({ attribute: false }) accessor retryCount = 0; } declare global { interface HTMLElementTagNameMap { 'affine-edgeless-image': ImageEdgelessBlockComponent; } }