import type { BlockCaptionEditor } from '@blocksuite/affine-components/caption'; import { LoadingIcon } from '@blocksuite/affine-components/icons'; import { Peekable } from '@blocksuite/affine-components/peek'; import { ResourceController } from '@blocksuite/affine-components/resource'; import { type ImageBlockModel, ImageBlockSchema, } from '@blocksuite/affine-model'; import { cssVarV2, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; import { formatSize } from '@blocksuite/affine-shared/utils'; import { BrokenImageIcon, ImageIcon } from '@blocksuite/icons/lit'; import { GfxBlockComponent } from '@blocksuite/std'; import { GfxViewInteractionExtension } from '@blocksuite/std/gfx'; import { computed } from '@preact/signals-core'; import { css, html } from 'lit'; import { query } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; import { when } from 'lit/directives/when.js'; import { copyImageBlob, downloadImageBlob, refreshData, turnImageIntoCardView, } from './utils'; @Peekable() export class ImageEdgelessBlockComponent extends GfxBlockComponent { static override styles = css` affine-edgeless-image { position: relative; } affine-edgeless-image .loading { display: flex; align-items: center; justify-content: center; position: absolute; top: 4px; right: 4px; width: 36px; height: 36px; padding: 5px; border-radius: 8px; background: ${unsafeCSSVarV2( 'loading/imageLoadingBackground', '#92929238' )}; & > svg { font-size: 25.71px; } } affine-edgeless-image .affine-image-status { position: absolute; left: 18px; bottom: 18px; } affine-edgeless-image .resizable-img, affine-edgeless-image .resizable-img img { width: 100%; height: 100%; } `; resourceController = new ResourceController( computed(() => this.model.props.sourceId$.value), 'Image' ); get blobUrl() { return this.resourceController.blobUrl$.value; } convertToCardView = () => { turnImageIntoCardView(this).catch(console.error); }; copy = () => { copyImageBlob(this).catch(console.error); }; download = () => { downloadImageBlob(this).catch(console.error); }; refreshData = () => { refreshData(this).catch(console.error); }; private _handleError() { this.resourceController.updateState({ errorMessage: 'Failed to download image!', }); } override connectedCallback() { super.connectedCallback(); this.contentEditable = 'false'; this.resourceController.setEngine(this.std.store.blobSync); this.disposables.add(this.resourceController.subscribe()); this.disposables.add(this.resourceController); this.disposables.add( this.model.props.sourceId$.subscribe(() => { this.refreshData(); }) ); } override renderGfxBlock() { const blobUrl = this.blobUrl; const { rotate = 0, size = 0, caption = 'Image' } = this.model.props; const containerStyleMap = styleMap({ display: 'flex', position: 'relative', width: '100%', height: '100%', transform: `rotate(${rotate}deg)`, transformOrigin: 'center', }); const resovledState = this.resourceController.resolveStateWith({ loadingIcon: LoadingIcon({ strokeColor: cssVarV2('button/pureWhiteText'), ringColor: cssVarV2('loading/imageLoadingLayer', '#ffffff8f'), }), errorIcon: BrokenImageIcon(), icon: ImageIcon(), title: 'Image', description: formatSize(size), }); const { loading, icon, description, error, needUpload } = resovledState; return html`
${when( blobUrl, () => html`
${caption}
${when(loading, () => html`
${icon}
`)} ${when( Boolean(error && description), () => html` needUpload ? this.resourceController.upload() : this.refreshData()} >` )} `, () => html`` )}
${Object.values(this.widgets)} `; } @query('block-caption-editor') accessor captionEditor!: BlockCaptionEditor | null; @query('.resizable-img') accessor resizableImg!: HTMLDivElement; } export const ImageEdgelessBlockInteraction = GfxViewInteractionExtension( ImageBlockSchema.model.flavour, { resizeConstraint: { lockRatio: true, }, } ); declare global { interface HTMLElementTagNameMap { 'affine-edgeless-image': ImageEdgelessBlockComponent; } }