Files
AFFiNE-Mirror/blocksuite/affine/blocks/image/src/image-edgeless-block.ts
fundon 5590cdd8f1 fix(editor): improve status display of attachments and images (#12573)
Closes: [BS-3564](https://linear.app/affine-design/issue/BS-3564/ui-embed-view-报错-ui-加-title)
Closes: [BS-3454](https://linear.app/affine-design/issue/BS-3454/点击-reload-后应该隐藏-attachment-embed-view-左下角-status(待新状态))

<img width="807" alt="Screenshot 2025-05-28 at 17 23 26" src="https://github.com/user-attachments/assets/9ecc29f8-73c6-4441-bc38-dfe9bd876542" />

<img width="820" alt="Screenshot 2025-05-28 at 17 45 37" src="https://github.com/user-attachments/assets/68e6db17-a814-4df4-a9fa-067ca03dec30" />

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

## Summary by CodeRabbit

- **New Features**
  - Added support for retrying failed uploads of attachments and images, allowing users to re-upload files directly from the error status interface.
  - The error status dialog now dynamically displays "Retry" for upload failures and "Reload" for download failures, with appropriate actions for each.
- **Enhancements**
  - Improved clarity and consistency in file type display and icon usage for attachments and citations.
  - Button labels in the attachment interface now have capitalized text for better readability.
- **Bug Fixes**
  - Streamlined error handling and status updates for attachment and image uploads/downloads, reducing redundant UI elements.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-29 02:18:51 +00:00

206 lines
5.4 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/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`
<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(loading, () => html`<div class="loading">${icon}</div>`)}
${when(
Boolean(error && description),
() =>
html`<affine-resource-status
class="affine-image-status"
.message=${description}
.needUpload=${needUpload}
.action=${() =>
needUpload
? this.resourceController.upload()
: 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;
}
}