mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat(editor): adjust attachment block UI (#11763)
Closes: [BS-3143](https://linear.app/affine-design/issue/BS-3143/更新-attachment-错误样式) Closes: [BS-3341](https://linear.app/affine-design/issue/BS-3341/attachment-select状态ui调整) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Improved attachment block with unified reactive state handling for loading, errors, and downloads. - Enhanced card layouts with modular rendering and dynamic buttons based on state. - **Bug Fixes** - Fixed UI state for "Embed view" action, now correctly disabled when not embedded. - Resolved attachment card styling issues and text overflow with new utility classes. - **Refactor** - Streamlined attachment block rendering and state management for better performance and maintainability. - Updated toolbar configuration for consistent parameter naming. - Simplified embedded and open methods for improved clarity. - Made internal functions private to reduce exported API surface. - **Chores** - Removed unused icon export and legacy upload tracking functions. - Updated attachment utilities to use reactive state, added data refresh, and improved error handling. - Refined image dimension handling for consistent resizing behavior. - Improved test selectors to target focused attachment containers for better reliability. - Refactored CSS to separate container and card styling and adopt theme-based colors consistently. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { getEmbedCardIcons } from '@blocksuite/affine-block-embed';
|
||||
import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
|
||||
import {
|
||||
AttachmentIcon16,
|
||||
getAttachmentFileIcon,
|
||||
} from '@blocksuite/affine-components/icons';
|
||||
CaptionedBlockComponent,
|
||||
SelectedStyle,
|
||||
} from '@blocksuite/affine-components/caption';
|
||||
import { getAttachmentFileIcon } from '@blocksuite/affine-components/icons';
|
||||
import { Peekable } from '@blocksuite/affine-components/peek';
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import {
|
||||
@@ -15,17 +15,22 @@ import {
|
||||
ThemeProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { humanFileSize } from '@blocksuite/affine-shared/utils';
|
||||
import { AttachmentIcon, ResetIcon, WarningIcon } from '@blocksuite/icons/lit';
|
||||
import { BlockSelection } from '@blocksuite/std';
|
||||
import { Slice } from '@blocksuite/store';
|
||||
import { html } from 'lit';
|
||||
import { type BlobState } from '@blocksuite/sync';
|
||||
import { effect, signal } from '@preact/signals-core';
|
||||
import { html, type TemplateResult } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { choose } from 'lit/directives/choose.js';
|
||||
import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { when } from 'lit/directives/when.js';
|
||||
|
||||
import { AttachmentEmbedProvider } from './embed';
|
||||
import { styles } from './styles';
|
||||
import { checkAttachmentBlob, downloadAttachmentBlob } from './utils';
|
||||
|
||||
import { downloadAttachmentBlob, refreshData } from './utils';
|
||||
type State = 'loading' | 'uploading' | 'warning' | 'oversize' | 'none';
|
||||
@Peekable({
|
||||
enableOn: ({ model }: AttachmentBlockComponent) => {
|
||||
return !model.doc.readonly && model.props.type.endsWith('pdf');
|
||||
@@ -36,6 +41,8 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
|
||||
blockDraggable = true;
|
||||
|
||||
blobState$ = signal<Partial<BlobState>>({});
|
||||
|
||||
protected containerStyleMap = styleMap({
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
@@ -63,26 +70,45 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
};
|
||||
|
||||
embedded = () => {
|
||||
return this.std
|
||||
.get(AttachmentEmbedProvider)
|
||||
.embedded(this.model, this._maxFileSize);
|
||||
return (
|
||||
Boolean(this.blobUrl) &&
|
||||
this.std
|
||||
.get(AttachmentEmbedProvider)
|
||||
.embedded(this.model, this._maxFileSize)
|
||||
);
|
||||
};
|
||||
|
||||
open = () => {
|
||||
if (!this.blobUrl) {
|
||||
return;
|
||||
}
|
||||
window.open(this.blobUrl, '_blank');
|
||||
const blobUrl = this.blobUrl;
|
||||
if (!blobUrl) return;
|
||||
window.open(blobUrl, '_blank');
|
||||
};
|
||||
|
||||
refreshData = () => {
|
||||
checkAttachmentBlob(this).catch(console.error);
|
||||
refreshData(this.std, this).catch(console.error);
|
||||
};
|
||||
|
||||
updateBlobState(state: Partial<BlobState>) {
|
||||
this.blobState$.value = { ...this.blobState$.value, ...state };
|
||||
}
|
||||
|
||||
determineState = (
|
||||
loading: boolean,
|
||||
uploading: boolean,
|
||||
overSize: boolean,
|
||||
error: boolean
|
||||
): State => {
|
||||
if (overSize) return 'oversize';
|
||||
if (error) return 'warning';
|
||||
if (uploading) return 'uploading';
|
||||
if (loading) return 'loading';
|
||||
return 'none';
|
||||
};
|
||||
|
||||
protected get embedView() {
|
||||
return this.std
|
||||
.get(AttachmentEmbedProvider)
|
||||
.render(this.model, this.blobUrl, this._maxFileSize);
|
||||
.render(this.model, this.blobUrl ?? undefined, this._maxFileSize);
|
||||
}
|
||||
|
||||
private _selectBlock() {
|
||||
@@ -96,9 +122,30 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.contentEditable = 'false';
|
||||
|
||||
this.refreshData();
|
||||
|
||||
this.contentEditable = 'false';
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
const blobId = this.model.props.sourceId$.value;
|
||||
if (!blobId) return;
|
||||
|
||||
const blobState$ = this.std.store.blobSync.blobState$(blobId);
|
||||
if (!blobState$) return;
|
||||
|
||||
const subscription = blobState$.subscribe(state => {
|
||||
if (state.overSize || state.errorMessage) {
|
||||
state.uploading = false;
|
||||
state.downloading = false;
|
||||
}
|
||||
|
||||
this.updateBlobState(state);
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
})
|
||||
);
|
||||
|
||||
if (!this.model.props.style) {
|
||||
this.doc.withoutTransact(() => {
|
||||
@@ -107,27 +154,12 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.model.propsUpdated.subscribe(({ key }) => {
|
||||
if (key === 'sourceId') {
|
||||
// Reset the blob url when the sourceId is changed
|
||||
if (this.blobUrl) {
|
||||
URL.revokeObjectURL(this.blobUrl);
|
||||
this.blobUrl = undefined;
|
||||
}
|
||||
this.refreshData();
|
||||
}
|
||||
});
|
||||
|
||||
// Workaround for https://github.com/toeverything/blocksuite/issues/4724
|
||||
this.disposables.add(
|
||||
this.std.get(ThemeProvider).theme$.subscribe(() => this.requestUpdate())
|
||||
);
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
if (this.blobUrl) {
|
||||
URL.revokeObjectURL(this.blobUrl);
|
||||
const blobUrl = this.blobUrl;
|
||||
if (blobUrl) {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
}
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
@@ -148,71 +180,173 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
}
|
||||
}
|
||||
|
||||
override renderBlock() {
|
||||
protected renderUpgradeButton = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
protected renderReloadButton = () => {
|
||||
return html`
|
||||
<button
|
||||
class="affine-attachment-content-button"
|
||||
@click=${(event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
this.refreshData();
|
||||
}}
|
||||
>
|
||||
${ResetIcon()} Reload
|
||||
</button>
|
||||
`;
|
||||
};
|
||||
|
||||
protected renderWithHorizontal(
|
||||
classInfo: ClassInfo,
|
||||
icon: TemplateResult,
|
||||
title: string,
|
||||
description: string,
|
||||
kind: TemplateResult,
|
||||
state: State
|
||||
) {
|
||||
return html`<div class=${classMap(classInfo)}>
|
||||
<div class="affine-attachment-content">
|
||||
<div class="affine-attachment-content-title">
|
||||
<div class="affine-attachment-content-title-icon">${icon}</div>
|
||||
|
||||
<div class="affine-attachment-content-title-text truncate">
|
||||
${title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="affine-attachment-content-description">
|
||||
<div class="affine-attachment-content-info truncate">
|
||||
${description}
|
||||
</div>
|
||||
${choose(state, [
|
||||
['oversize', this.renderUpgradeButton],
|
||||
['warning', this.renderReloadButton],
|
||||
])}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="affine-attachment-banner">${kind}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected renderWithVertical(
|
||||
classInfo: ClassInfo,
|
||||
icon: TemplateResult,
|
||||
title: string,
|
||||
description: string,
|
||||
kind: TemplateResult,
|
||||
state?: State
|
||||
) {
|
||||
return html`<div class=${classMap(classInfo)}>
|
||||
<div class="affine-attachment-content">
|
||||
<div class="affine-attachment-content-title">
|
||||
<div class="affine-attachment-content-title-icon">${icon}</div>
|
||||
|
||||
<div class="affine-attachment-content-title-text truncate">
|
||||
${title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="affine-attachment-content-info truncate">
|
||||
${description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="affine-attachment-banner">
|
||||
${kind}
|
||||
${choose(state, [
|
||||
['oversize', this.renderUpgradeButton],
|
||||
['warning', this.renderReloadButton],
|
||||
])}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected renderCard = () => {
|
||||
const { name, size, style } = this.model.props;
|
||||
const cardStyle = style ?? AttachmentBlockStyles[1];
|
||||
|
||||
const theme = this.std.get(ThemeProvider).theme;
|
||||
const theme = this.std.get(ThemeProvider).theme$.value;
|
||||
const { LoadingIcon } = getEmbedCardIcons(theme);
|
||||
|
||||
const titleIcon = this.loading ? LoadingIcon : AttachmentIcon16;
|
||||
const titleText = this.loading ? 'Loading...' : name;
|
||||
const infoText = this.error ? 'File loading failed.' : humanFileSize(size);
|
||||
const blobState = this.blobState$.value;
|
||||
const {
|
||||
uploading = false,
|
||||
downloading = false,
|
||||
overSize = false,
|
||||
errorMessage,
|
||||
} = blobState;
|
||||
const warning = !overSize && Boolean(errorMessage);
|
||||
const error = overSize || warning;
|
||||
const loading = !error && downloading;
|
||||
const state = this.determineState(loading, uploading, overSize, error);
|
||||
|
||||
const fileType = name.split('.').pop() ?? '';
|
||||
const FileTypeIcon = getAttachmentFileIcon(fileType);
|
||||
const classInfo = {
|
||||
'affine-attachment-card': true,
|
||||
[cardStyle]: true,
|
||||
error,
|
||||
loading,
|
||||
};
|
||||
|
||||
const embedView = this.embedView;
|
||||
const icon = loading
|
||||
? LoadingIcon
|
||||
: error
|
||||
? WarningIcon()
|
||||
: AttachmentIcon();
|
||||
const title = uploading ? 'Uploading...' : loading ? 'Loading...' : name;
|
||||
const description = errorMessage || humanFileSize(size);
|
||||
const kind = getAttachmentFileIcon(name.split('.').pop() ?? '');
|
||||
|
||||
return when(
|
||||
cardStyle === 'cubeThick',
|
||||
() =>
|
||||
this.renderWithVertical(
|
||||
classInfo,
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
kind,
|
||||
state
|
||||
),
|
||||
() =>
|
||||
this.renderWithHorizontal(
|
||||
classInfo,
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
kind,
|
||||
state
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
override renderBlock() {
|
||||
return html`
|
||||
<div class="affine-attachment-container" style=${this.containerStyleMap}>
|
||||
${embedView
|
||||
? html`<div class="affine-attachment-embed-container">
|
||||
${embedView}
|
||||
</div>`
|
||||
: html`<div
|
||||
class=${classMap({
|
||||
'affine-attachment-card': true,
|
||||
[cardStyle]: true,
|
||||
loading: this.loading,
|
||||
error: this.error,
|
||||
unsynced: false,
|
||||
})}
|
||||
>
|
||||
<div class="affine-attachment-content">
|
||||
<div class="affine-attachment-content-title">
|
||||
<div class="affine-attachment-content-title-icon">
|
||||
${titleIcon}
|
||||
</div>
|
||||
|
||||
<div class="affine-attachment-content-title-text">
|
||||
${titleText}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="affine-attachment-content-info">${infoText}</div>
|
||||
</div>
|
||||
|
||||
<div class="affine-attachment-banner">${FileTypeIcon}</div>
|
||||
</div>`}
|
||||
<div
|
||||
class=${classMap({
|
||||
'affine-attachment-container': true,
|
||||
focused: this.selected$.value,
|
||||
})}
|
||||
style=${this.containerStyleMap}
|
||||
>
|
||||
${when(
|
||||
this.embedView,
|
||||
() =>
|
||||
html`<div class="affine-attachment-embed-container">
|
||||
${this.embedView}
|
||||
</div>`,
|
||||
this.renderCard
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor allowEmbed = false;
|
||||
accessor blobUrl: string | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor blobUrl: string | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor downloading = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor error = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor loading = false;
|
||||
override accessor selectedStyle = SelectedStyle.Border;
|
||||
|
||||
override accessor useCaptionEditor = true;
|
||||
}
|
||||
|
||||
@@ -69,6 +69,10 @@ export const attachmentViewDropdownMenu = {
|
||||
{
|
||||
id: 'embed',
|
||||
label: 'Embed view',
|
||||
disabled: ctx => {
|
||||
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
|
||||
return block ? !block.embedded() : true;
|
||||
},
|
||||
run(ctx) {
|
||||
const model = ctx.getCurrentModelByType(AttachmentBlockModel);
|
||||
if (!model) return;
|
||||
@@ -156,24 +160,24 @@ const builtinToolbarConfig = {
|
||||
actions: [
|
||||
{
|
||||
id: 'a.rename',
|
||||
content(cx) {
|
||||
const block = cx.getCurrentBlockByType(AttachmentBlockComponent);
|
||||
content(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
|
||||
if (!block) return null;
|
||||
|
||||
const abortController = new AbortController();
|
||||
abortController.signal.onabort = () => cx.show();
|
||||
abortController.signal.onabort = () => ctx.show();
|
||||
|
||||
return html`
|
||||
<editor-icon-button
|
||||
aria-label="Rename"
|
||||
.tooltip="${'Rename'}"
|
||||
@click=${() => {
|
||||
cx.hide();
|
||||
ctx.hide();
|
||||
|
||||
createLitPortal({
|
||||
template: RenameModal({
|
||||
model: block.model,
|
||||
editorHost: cx.host,
|
||||
editorHost: ctx.host,
|
||||
abortController,
|
||||
}),
|
||||
computePosition: {
|
||||
|
||||
@@ -187,7 +187,7 @@ const embedConfig: AttachmentEmbedConfig[] = [
|
||||
/**
|
||||
* Turn the attachment block into an image block.
|
||||
*/
|
||||
export async function turnIntoImageBlock(model: AttachmentBlockModel) {
|
||||
async function turnIntoImageBlock(model: AttachmentBlockModel) {
|
||||
if (!model.doc.schema.flavourSchemaMap.has('affine:image')) {
|
||||
console.error('The image flavour is not supported!');
|
||||
return;
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { css } from 'lit';
|
||||
|
||||
export const styles = css`
|
||||
.affine-attachment-card {
|
||||
margin: 0 auto;
|
||||
.affine-attachment-container {
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
border: 1px solid ${unsafeCSSVarV2('layer/background/tertiary')};
|
||||
background: ${unsafeCSSVarV2('layer/background/primary')};
|
||||
overflow: hidden;
|
||||
|
||||
&.focused {
|
||||
border-color: ${unsafeCSSVarV2('layer/insideBorder/primaryBorder')};
|
||||
}
|
||||
}
|
||||
|
||||
.affine-attachment-card {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--affine-background-tertiary-color);
|
||||
|
||||
opacity: var(--add, 1);
|
||||
background: var(--affine-background-primary-color);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.affine-attachment-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
flex: 1 0 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.affine-attachment-content-title {
|
||||
@@ -33,7 +35,6 @@ export const styles = css`
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
@@ -43,24 +44,18 @@ export const styles = css`
|
||||
height: 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--affine-text-primary-color);
|
||||
}
|
||||
|
||||
.affine-attachment-content-title-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: var(--affine-background-primary-color);
|
||||
.truncate {
|
||||
align-self: stretch;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.affine-attachment-content-title-text {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
word-break: break-all;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--affine-text-primary-color);
|
||||
|
||||
font-family: var(--affine-font-family);
|
||||
font-size: var(--affine-font-sm);
|
||||
font-style: normal;
|
||||
@@ -68,17 +63,15 @@ export const styles = css`
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.affine-attachment-content-description {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.affine-attachment-content-info {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
flex: 1 0 0;
|
||||
|
||||
word-break: break-all;
|
||||
overflow: hidden;
|
||||
color: var(--affine-text-secondary-color);
|
||||
text-overflow: ellipsis;
|
||||
|
||||
font-family: var(--affine-font-family);
|
||||
font-size: var(--affine-font-xs);
|
||||
font-style: normal;
|
||||
@@ -86,6 +79,26 @@ export const styles = css`
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.affine-attachment-content-button {
|
||||
display: flex;
|
||||
height: 20px;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
padding: 0 4px;
|
||||
color: ${unsafeCSSVarV2('button/primary')};
|
||||
font-family: var(--affine-font-family);
|
||||
font-size: var(--affine-font-xs);
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
|
||||
svg {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.affine-attachment-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -93,16 +106,15 @@ export const styles = css`
|
||||
}
|
||||
|
||||
.affine-attachment-card.loading {
|
||||
background: var(--affine-background-secondary-color);
|
||||
|
||||
.affine-attachment-content-title-text {
|
||||
color: var(--affine-placeholder-color);
|
||||
}
|
||||
}
|
||||
|
||||
.affine-attachment-card.error,
|
||||
.affine-attachment-card.unsynced {
|
||||
background: var(--affine-background-secondary-color);
|
||||
.affine-attachment-card.error {
|
||||
.affine-attachment-content-title-icon {
|
||||
color: ${unsafeCSSVarV2('status/error')};
|
||||
}
|
||||
}
|
||||
|
||||
.affine-attachment-card.cubeThick {
|
||||
@@ -116,7 +128,7 @@ export const styles = css`
|
||||
}
|
||||
|
||||
.affine-attachment-banner {
|
||||
justify-content: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,128 +21,19 @@ import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import type { AttachmentBlockComponent } from './attachment-block';
|
||||
|
||||
const attachmentUploads = new Set<string>();
|
||||
export function setAttachmentUploading(blockId: string) {
|
||||
attachmentUploads.add(blockId);
|
||||
}
|
||||
export function setAttachmentUploaded(blockId: string) {
|
||||
attachmentUploads.delete(blockId);
|
||||
}
|
||||
function isAttachmentUploading(blockId: string) {
|
||||
return attachmentUploads.has(blockId);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function will not verify the size of the file.
|
||||
*/
|
||||
// TODO(@fundon): should remove
|
||||
export async function uploadAttachmentBlob(
|
||||
std: BlockStdScope,
|
||||
blockId: string,
|
||||
blob: Blob,
|
||||
filetype: string,
|
||||
isEdgeless?: boolean
|
||||
): Promise<void> {
|
||||
if (isAttachmentUploading(blockId)) return;
|
||||
|
||||
let sourceId: string | undefined;
|
||||
|
||||
try {
|
||||
setAttachmentUploading(blockId);
|
||||
sourceId = await std.store.blobSync.set(blob);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof Error) {
|
||||
toast(
|
||||
std.host,
|
||||
`Failed to upload attachment! ${error.message || error.toString()}`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setAttachmentUploaded(blockId);
|
||||
|
||||
const block = std.store.getBlock(blockId);
|
||||
|
||||
std.store.withoutTransact(() => {
|
||||
if (!block) return;
|
||||
|
||||
std.store.updateBlock(block.model, {
|
||||
sourceId,
|
||||
} satisfies Partial<AttachmentBlockProps>);
|
||||
});
|
||||
|
||||
std.getOptional(TelemetryProvider)?.track('AttachmentUploadedEvent', {
|
||||
page: `${isEdgeless ? 'whiteboard' : 'doc'} editor`,
|
||||
module: 'attachment',
|
||||
segment: 'attachment',
|
||||
control: 'uploader',
|
||||
type: filetype,
|
||||
category: block && sourceId ? 'success' : 'failure',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAttachmentBlob(model: AttachmentBlockModel) {
|
||||
const sourceId = model.props.sourceId;
|
||||
if (!sourceId) {
|
||||
return null;
|
||||
}
|
||||
const {
|
||||
sourceId$: { value: sourceId },
|
||||
type$: { value: type },
|
||||
} = model.props;
|
||||
if (!sourceId) return null;
|
||||
|
||||
const doc = model.doc;
|
||||
let blob = await doc.blobSync.get(sourceId);
|
||||
|
||||
if (blob) {
|
||||
blob = new Blob([blob], { type: model.props.type });
|
||||
}
|
||||
if (!blob) return null;
|
||||
|
||||
return blob;
|
||||
}
|
||||
|
||||
// TODO(@fundon): should remove
|
||||
export async function checkAttachmentBlob(block: AttachmentBlockComponent) {
|
||||
const model = block.model;
|
||||
const { id } = model;
|
||||
const { sourceId } = model.props;
|
||||
|
||||
if (isAttachmentUploading(id)) {
|
||||
block.loading = true;
|
||||
block.error = false;
|
||||
block.allowEmbed = false;
|
||||
if (block.blobUrl) {
|
||||
URL.revokeObjectURL(block.blobUrl);
|
||||
block.blobUrl = undefined;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!sourceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = await getAttachmentBlob(model);
|
||||
if (!blob) {
|
||||
return;
|
||||
}
|
||||
|
||||
block.loading = false;
|
||||
block.error = false;
|
||||
block.allowEmbed = block.embedded();
|
||||
if (block.blobUrl) {
|
||||
URL.revokeObjectURL(block.blobUrl);
|
||||
}
|
||||
block.blobUrl = URL.createObjectURL(blob);
|
||||
} catch (error) {
|
||||
console.warn(error, model, sourceId);
|
||||
|
||||
block.loading = false;
|
||||
block.error = true;
|
||||
block.allowEmbed = false;
|
||||
if (block.blobUrl) {
|
||||
URL.revokeObjectURL(block.blobUrl);
|
||||
block.blobUrl = undefined;
|
||||
}
|
||||
}
|
||||
return new Blob([blob], { type });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,26 +41,22 @@ export async function checkAttachmentBlob(block: AttachmentBlockComponent) {
|
||||
* the download process may take a long time!
|
||||
*/
|
||||
export function downloadAttachmentBlob(block: AttachmentBlockComponent) {
|
||||
const { host, model, loading, error, downloading, blobUrl } = block;
|
||||
if (downloading) {
|
||||
toast(host, 'Download in progress...');
|
||||
return;
|
||||
}
|
||||
const { host, model, blobUrl, blobState$ } = block;
|
||||
|
||||
if (loading) {
|
||||
toast(host, 'Please wait, file is loading...');
|
||||
if (blobState$.peek().downloading) {
|
||||
toast(host, 'Download in progress...');
|
||||
return;
|
||||
}
|
||||
|
||||
const name = model.props.name;
|
||||
const shortName = name.length < 20 ? name : name.slice(0, 20) + '...';
|
||||
|
||||
if (error || !blobUrl) {
|
||||
if (!blobUrl) {
|
||||
toast(host, `Failed to download ${shortName}!`);
|
||||
return;
|
||||
}
|
||||
|
||||
block.downloading = true;
|
||||
block.updateBlobState({ downloading: true });
|
||||
|
||||
toast(host, `Downloading ${shortName}`);
|
||||
|
||||
@@ -180,7 +67,34 @@ export function downloadAttachmentBlob(block: AttachmentBlockComponent) {
|
||||
tmpLink.dispatchEvent(event);
|
||||
tmpLink.remove();
|
||||
|
||||
block.downloading = false;
|
||||
block.updateBlobState({ downloading: false });
|
||||
}
|
||||
|
||||
export async function refreshData(
|
||||
std: BlockStdScope,
|
||||
block: AttachmentBlockComponent
|
||||
) {
|
||||
const model = block.model;
|
||||
const sourceId = model.props.sourceId$.peek();
|
||||
if (!sourceId) return;
|
||||
|
||||
const blobUrl = block.blobUrl;
|
||||
if (blobUrl) {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
block.blobUrl = null;
|
||||
}
|
||||
|
||||
let blob = await std.store.blobSync.get(sourceId);
|
||||
if (!blob) {
|
||||
block.updateBlobState({ errorMessage: 'File not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const type = model.props.type$.peek();
|
||||
|
||||
blob = new Blob([blob], { type });
|
||||
|
||||
block.blobUrl = URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
export async function getFileType(file: File) {
|
||||
@@ -219,6 +133,7 @@ async function buildPropsWith(
|
||||
|
||||
try {
|
||||
const { name, size } = file;
|
||||
// TODO(@fundon): should re-upload when upload timeout
|
||||
const sourceId = await std.store.blobSync.set(file);
|
||||
type = await getFileType(file);
|
||||
|
||||
@@ -233,6 +148,7 @@ async function buildPropsWith(
|
||||
category = 'failure';
|
||||
throw err;
|
||||
} finally {
|
||||
// TODO(@fundon): should change event name because this is just a local operation.
|
||||
std.getOptional(TelemetryProvider)?.track('AttachmentUploadedEvent', {
|
||||
page: `${mode} editor`,
|
||||
module: 'attachment',
|
||||
@@ -303,7 +219,6 @@ export async function addAttachments(
|
||||
const gap = 32;
|
||||
const width = EMBED_CARD_WIDTH.cubeThick;
|
||||
const height = EMBED_CARD_HEIGHT.cubeThick;
|
||||
|
||||
const flavour = AttachmentBlockSchema.model.flavour;
|
||||
|
||||
const blocks = propsArray.map((props, index) => {
|
||||
@@ -312,7 +227,7 @@ export async function addAttachments(
|
||||
return { flavour, blockProps: { ...props, style, xywh } };
|
||||
});
|
||||
|
||||
const blockIds = std.store.addBlocks(blocks);
|
||||
const blockIds = std.store.addBlocks(blocks, gfx.surface);
|
||||
|
||||
gfx.selection.set({
|
||||
elements: blockIds,
|
||||
|
||||
@@ -481,10 +481,12 @@ export async function addImages(
|
||||
|
||||
// If maxWidth is provided, limit the width of the image to maxWidth
|
||||
// Otherwise, use the original width
|
||||
const width = maxWidth ? Math.min(props.width, maxWidth) : props.width;
|
||||
const height = maxWidth
|
||||
? (props.height / props.width) * width
|
||||
: props.height;
|
||||
if (maxWidth) {
|
||||
const p = props.height / props.width;
|
||||
props.width = Math.min(props.width, maxWidth);
|
||||
props.height = props.width * p;
|
||||
}
|
||||
const { width, height } = props;
|
||||
|
||||
const xywh = calcBoundByOrigin(
|
||||
center,
|
||||
|
||||
@@ -340,11 +340,6 @@ export const FontFamilyIcon = icons.FontIcon({
|
||||
height: '20',
|
||||
});
|
||||
|
||||
export const AttachmentIcon16 = icons.AttachmentIcon({
|
||||
width: '16',
|
||||
height: '16',
|
||||
});
|
||||
|
||||
export const TextBackgroundDuotoneIcon = html` <svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
|
||||
Reference in New Issue
Block a user