mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-31 09:09:54 +08:00
9465d0dc73
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 -->
224 lines
6.8 KiB
TypeScript
224 lines
6.8 KiB
TypeScript
import {
|
|
getAttachmentFileIcon,
|
|
LoadingIcon,
|
|
WebIcon16,
|
|
} from '@blocksuite/affine-components/icons';
|
|
import type { FootNote } from '@blocksuite/affine-model';
|
|
import { ImageProxyService } from '@blocksuite/affine-shared/adapters';
|
|
import {
|
|
DocDisplayMetaProvider,
|
|
LinkPreviewServiceIdentifier,
|
|
} from '@blocksuite/affine-shared/services';
|
|
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
|
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
|
import type { BlockStdScope } from '@blocksuite/std';
|
|
import { computed, signal } from '@preact/signals-core';
|
|
import { baseTheme } from '@toeverything/theme';
|
|
import { css, html, LitElement, nothing, unsafeCSS } from 'lit';
|
|
import { property } from 'lit/decorators.js';
|
|
|
|
import type { FootNotePopupClickHandler } from './footnote-config';
|
|
|
|
export class FootNotePopup extends SignalWatcher(WithDisposable(LitElement)) {
|
|
static override styles = css`
|
|
.footnote-popup-container {
|
|
border-radius: 8px;
|
|
box-shadow: ${unsafeCSSVar('overlayPanelShadow')};
|
|
background-color: ${unsafeCSSVarV2('layer/background/primary')};
|
|
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
|
|
max-width: 260px;
|
|
padding: 4px 8px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
transition: 0.3s ease-in-out;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.footnote-popup-description {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
|
|
font-size: var(--affine-font-xs);
|
|
font-style: normal;
|
|
font-weight: 400;
|
|
line-height: 20px;
|
|
height: 20px;
|
|
}
|
|
`;
|
|
|
|
private readonly _isLoading$ = signal(false);
|
|
|
|
private readonly _linkPreview$ = signal<{
|
|
favicon: string | undefined;
|
|
title?: string;
|
|
description?: string;
|
|
}>({
|
|
favicon: undefined,
|
|
title: undefined,
|
|
description: undefined,
|
|
});
|
|
|
|
private readonly _prefixIcon$ = computed(() => {
|
|
const referenceType = this.footnote.reference.type;
|
|
if (referenceType === 'doc') {
|
|
const docId = this.footnote.reference.docId;
|
|
if (!docId) {
|
|
return undefined;
|
|
}
|
|
return this.std.get(DocDisplayMetaProvider).icon(docId).value;
|
|
} else if (referenceType === 'attachment') {
|
|
const fileType = this.footnote.reference.fileType;
|
|
if (!fileType) {
|
|
return undefined;
|
|
}
|
|
return getAttachmentFileIcon(fileType);
|
|
} else if (referenceType === 'url') {
|
|
if (this._isLoading$.value) {
|
|
return LoadingIcon();
|
|
}
|
|
|
|
const favicon = this._linkPreview$.value?.favicon;
|
|
const imageSrc = favicon
|
|
? this.imageProxyService.buildUrl(favicon)
|
|
: undefined;
|
|
return imageSrc ? html`<img src=${imageSrc} alt="favicon" />` : WebIcon16;
|
|
}
|
|
return undefined;
|
|
});
|
|
|
|
private readonly _popupLabel$ = computed(() => {
|
|
const referenceType = this.footnote.reference.type;
|
|
let label = '';
|
|
const { docId, fileName, url } = this.footnote.reference;
|
|
switch (referenceType) {
|
|
case 'doc':
|
|
if (!docId) {
|
|
return label;
|
|
}
|
|
label = this.std.get(DocDisplayMetaProvider).title(docId).value;
|
|
break;
|
|
case 'attachment':
|
|
if (!fileName) {
|
|
return label;
|
|
}
|
|
label = fileName;
|
|
break;
|
|
case 'url':
|
|
if (!url) {
|
|
return label;
|
|
}
|
|
label = this._linkPreview$.value?.title ?? url;
|
|
break;
|
|
}
|
|
return label;
|
|
});
|
|
|
|
private readonly _tooltip$ = computed(() => {
|
|
const referenceType = this.footnote.reference.type;
|
|
if (referenceType === 'url') {
|
|
const title = this._linkPreview$.value?.title;
|
|
const url = this.footnote.reference.url;
|
|
return [title, url].filter(Boolean).join('\n') || '';
|
|
}
|
|
return this._popupLabel$.value;
|
|
});
|
|
|
|
private readonly _onClick = () => {
|
|
this.onPopupClick(this.footnote, this.abortController);
|
|
this.abortController.abort();
|
|
};
|
|
|
|
private readonly _initLinkPreviewData = () => {
|
|
this._linkPreview$.value = {
|
|
favicon: this.footnote.reference.favicon,
|
|
title: this.footnote.reference.title,
|
|
description: this.footnote.reference.description,
|
|
};
|
|
};
|
|
|
|
override connectedCallback() {
|
|
super.connectedCallback();
|
|
|
|
this._initLinkPreviewData();
|
|
|
|
// If the reference is a url, and the url exists
|
|
// and the link preview data is not already set, fetch the link preview data
|
|
const isTitleAndDescriptionEmpty =
|
|
!this._linkPreview$.value?.title &&
|
|
!this._linkPreview$.value?.description;
|
|
if (
|
|
this.footnote.reference.type === 'url' &&
|
|
this.footnote.reference.url &&
|
|
isTitleAndDescriptionEmpty
|
|
) {
|
|
this._isLoading$.value = true;
|
|
this.std
|
|
.get(LinkPreviewServiceIdentifier)
|
|
.query(this.footnote.reference.url)
|
|
.then(data => {
|
|
// update the local link preview data
|
|
this._linkPreview$.value = {
|
|
favicon: data.icon ?? undefined,
|
|
title: data.title ?? undefined,
|
|
description: data.description ?? undefined,
|
|
};
|
|
|
|
// update the footnote attributes in the node with the link preview data
|
|
// to avoid fetching the same data multiple times
|
|
const footnote: FootNote = {
|
|
...this.footnote,
|
|
reference: {
|
|
...this.footnote.reference,
|
|
...(data.icon && { favicon: data.icon }),
|
|
...(data.title && { title: data.title }),
|
|
...(data.description && { description: data.description }),
|
|
},
|
|
};
|
|
this.updateFootnoteAttributes(footnote);
|
|
})
|
|
.catch(console.error)
|
|
.finally(() => {
|
|
this._isLoading$.value = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
override render() {
|
|
const description = this._linkPreview$.value?.description;
|
|
|
|
return html`
|
|
<div class="footnote-popup-container" @click=${this._onClick}>
|
|
<footnote-popup-chip
|
|
.prefixIcon=${this._prefixIcon$.value}
|
|
.label=${this._popupLabel$.value}
|
|
.tooltip=${this._tooltip$.value}
|
|
></footnote-popup-chip>
|
|
${description
|
|
? html` <div class="footnote-popup-description">${description}</div> `
|
|
: nothing}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
get imageProxyService() {
|
|
return this.std.get(ImageProxyService);
|
|
}
|
|
|
|
@property({ attribute: false })
|
|
accessor footnote!: FootNote;
|
|
|
|
@property({ attribute: false })
|
|
accessor std!: BlockStdScope;
|
|
|
|
@property({ attribute: false })
|
|
accessor abortController!: AbortController;
|
|
|
|
@property({ attribute: false })
|
|
accessor onPopupClick: FootNotePopupClickHandler | (() => void) = () => {};
|
|
|
|
@property({ attribute: false })
|
|
accessor updateFootnoteAttributes: (footnote: FootNote) => void = () => {};
|
|
}
|