Files
AFFiNE-Mirror/blocksuite/affine/blocks/image/src/image-edgeless-block.ts
T
fundon d0539fde22 fix(editor): unify file size formatting method (#12444)
Closes: [BS-3524](https://linear.app/affine-design/issue/BS-3524/统一文件大小单位,与-af-一致)

![Screenshot 2025-05-22 at 15.09.41.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/8ypiIKZXudF5a0tIgIzf/ddb6fa38-243f-4e65-b572-d476e3771d74.png)

![Screenshot 2025-05-22 at 15.09.48.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/8ypiIKZXudF5a0tIgIzf/5d182332-4d0a-419a-b206-df58552fe740.png)

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

- **Refactor**
  - Updated file size formatting throughout the app to use a new, consistent utility for displaying file sizes.
  - Improved clarity and uniformity of file size information in attachments, images, and related notifications.
  - Enhanced type support to explicitly allow null values for file size descriptions.
- **Bug Fixes**
  - Adjusted file size display in tests to match updated formatting standards.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-23 06:33:31 +00:00

197 lines
5.2 KiB
TypeScript

import type { BlockCaptionEditor } from '@blocksuite/affine-components/caption';
import { getLoadingIconWith } 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 { ThemeProvider } from '@blocksuite/affine-shared/services';
import { 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: 20px;
height: 20px;
padding: 4px;
border-radius: 4px;
background: ${unsafeCSSVarV2('loading/backgroundLayer')};
}
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 theme = this.std.get(ThemeProvider).theme$.value;
const loadingIcon = getLoadingIconWith(theme);
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,
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">${loadingIcon}</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;
}
}