Files
AFFiNE-Mirror/blocksuite/affine/blocks/image/src/image-edgeless-block.ts
T
fundon 9465d0dc73 fix(editor): loading style (#12537)
Closes: [BS-3555](https://linear.app/affine-design/issue/BS-3555/ui-attachment-loading-变量更新)
Closes: [BS-3559](https://linear.app/affine-design/issue/BS-3559/ui-图片-loading-变量更新)

### Dark
<img width="625" alt="Screenshot 2025-05-26 at 20 32 36" src="https://github.com/user-attachments/assets/93501e3d-8fc6-45f9-84a0-ac147e5c5f9f" />

### Light
<img width="623" alt="Screenshot 2025-05-26 at 20 32 25" src="https://github.com/user-attachments/assets/7d5bc128-6667-45b5-982d-dab3a22706a7" />

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

- **New Features**
  - Loading icons are now invoked as functions, allowing for more flexible and customizable rendering with parameters like size and progress.

- **Refactor**
  - Replaced theme-dependent and static loading icon references with a unified `LoadingIcon()` component across multiple components and blocks.
  - Removed legacy icon variants and simplified icon import statements, centralizing icon rendering logic.

- **Style**
  - Updated styles for loading and reload buttons to use theme-aware CSS variables.
  - Enlarged and repositioned loading indicators in image blocks for better visibility.

- **Bug Fixes**
  - Achieved consistent loading icon rendering across various blocks and components by standardizing icon invocation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 05:10:27 +00:00

199 lines
5.2 KiB
TypeScript

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<ImageBlockModel> {
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/backgroundLayer')};
& > 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'),
}),
errorIcon: BrokenImageIcon(),
icon: ImageIcon(),
title: 'Image',
description: formatSize(size),
});
return html`
<div class="affine-image-container" style=${containerStyleMap}>
${when(
blobUrl,
() => html`
<div class="resizable-img">
<img
class="drag-target"
draggable="false"
loading="lazy"
src=${blobUrl}
alt=${caption}
@error=${this._handleError}
/>
</div>
${when(
resovledState.loading,
() => html`<div class="loading">${resovledState.icon}</div>`
)}
${when(
resovledState.error && resovledState.description,
() =>
html`<affine-resource-status
class="affine-image-status"
.message=${resovledState.description}
.reload=${() => this.refreshData()}
></affine-resource-status>`
)}
`,
() =>
html`<affine-image-fallback-card
.state=${resovledState}
></affine-image-fallback-card>`
)}
<affine-block-selection .block=${this}></affine-block-selection>
</div>
<block-caption-editor></block-caption-editor>
${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;
}
}