mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 20:38:52 +00:00
refactor(editor): extract attachment block (#9308)
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
"author": "toeverything",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@blocksuite/affine-block-attachment": "workspace:*",
|
||||
"@blocksuite/affine-block-bookmark": "workspace:*",
|
||||
"@blocksuite/affine-block-embed": "workspace:*",
|
||||
"@blocksuite/affine-block-list": "workspace:*",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { attachmentBlockNotionHtmlAdapterMatcher } from '@blocksuite/affine-block-attachment';
|
||||
import { bookmarkBlockNotionHtmlAdapterMatcher } from '@blocksuite/affine-block-bookmark';
|
||||
import {
|
||||
embedFigmaBlockNotionHtmlAdapterMatcher,
|
||||
@@ -9,7 +10,6 @@ import { listBlockNotionHtmlAdapterMatcher } from '@blocksuite/affine-block-list
|
||||
import { paragraphBlockNotionHtmlAdapterMatcher } from '@blocksuite/affine-block-paragraph';
|
||||
import type { BlockNotionHtmlAdapterMatcher } from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
import { attachmentBlockNotionHtmlAdapterMatcher } from '../../../attachment-block/adapters/notion-html.js';
|
||||
import { codeBlockNotionHtmlAdapterMatcher } from '../../../code-block/adapters/notion-html.js';
|
||||
import { databaseBlockNotionHtmlAdapterMatcher } from '../../../database-block/adapters/notion-html.js';
|
||||
import { dividerBlockNotionHtmlAdapterMatcher } from '../../../divider-block/adapters/notion-html.js';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AttachmentBlockSpec } from '@blocksuite/affine-block-attachment';
|
||||
import { BookmarkBlockSpec } from '@blocksuite/affine-block-bookmark';
|
||||
import { EmbedExtensions } from '@blocksuite/affine-block-embed';
|
||||
import { ListBlockSpec } from '@blocksuite/affine-block-list';
|
||||
@@ -7,7 +8,6 @@ import { EditPropsStore } from '@blocksuite/affine-shared/services';
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
|
||||
import { AdapterFactoryExtensions } from '../_common/adapters/extension.js';
|
||||
import { AttachmentBlockSpec } from '../attachment-block/attachment-spec.js';
|
||||
import { CodeBlockSpec } from '../code-block/code-block-spec.js';
|
||||
import { DataViewBlockSpec } from '../data-view-block/data-view-spec.js';
|
||||
import { DatabaseBlockSpec } from '../database-block/database-spec.js';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AttachmentBlockSpec } from '@blocksuite/affine-block-attachment';
|
||||
import { BookmarkBlockSpec } from '@blocksuite/affine-block-bookmark';
|
||||
import {
|
||||
EmbedFigmaBlockSpec,
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
import { ListBlockSpec } from '@blocksuite/affine-block-list';
|
||||
import { ParagraphBlockSpec } from '@blocksuite/affine-block-paragraph';
|
||||
|
||||
import { AttachmentBlockSpec } from '../../attachment-block/attachment-spec.js';
|
||||
import { CodeBlockSpec } from '../../code-block/code-block-spec.js';
|
||||
import { DataViewBlockSpec } from '../../data-view-block/data-view-spec.js';
|
||||
import { DatabaseBlockSpec } from '../../database-block/database-spec.js';
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import { AttachmentBlockSchema } from '@blocksuite/affine-model';
|
||||
import {
|
||||
BlockNotionHtmlAdapterExtension,
|
||||
type BlockNotionHtmlAdapterMatcher,
|
||||
FetchUtils,
|
||||
HastUtils,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { getFilenameFromContentDisposition } from '@blocksuite/affine-shared/utils';
|
||||
import { sha } from '@blocksuite/global/utils';
|
||||
import { getAssetName, nanoid } from '@blocksuite/store';
|
||||
|
||||
export const attachmentBlockNotionHtmlAdapterMatcher: BlockNotionHtmlAdapterMatcher =
|
||||
{
|
||||
flavour: AttachmentBlockSchema.model.flavour,
|
||||
toMatch: o => {
|
||||
return (
|
||||
HastUtils.isElement(o.node) &&
|
||||
o.node.tagName === 'figure' &&
|
||||
!!HastUtils.querySelector(o.node, '.source')
|
||||
);
|
||||
},
|
||||
fromMatch: () => false,
|
||||
toBlockSnapshot: {
|
||||
enter: async (o, context) => {
|
||||
if (!HastUtils.isElement(o.node)) {
|
||||
return;
|
||||
}
|
||||
const { assets, walkerContext } = context;
|
||||
if (!assets) {
|
||||
return;
|
||||
}
|
||||
|
||||
const embededFigureWrapper = HastUtils.querySelector(o.node, '.source');
|
||||
let embededURL = '';
|
||||
if (embededFigureWrapper) {
|
||||
const embedA = HastUtils.querySelector(embededFigureWrapper, 'a');
|
||||
embededURL =
|
||||
typeof embedA?.properties.href === 'string'
|
||||
? embedA.properties.href
|
||||
: '';
|
||||
}
|
||||
if (embededURL) {
|
||||
let blobId = '';
|
||||
let name = '';
|
||||
let type = '';
|
||||
let size = 0;
|
||||
if (!FetchUtils.fetchable(embededURL)) {
|
||||
const embededURLSplit = embededURL.split('/');
|
||||
while (embededURLSplit.length > 0) {
|
||||
const key = assets
|
||||
.getPathBlobIdMap()
|
||||
.get(decodeURIComponent(embededURLSplit.join('/')));
|
||||
if (key) {
|
||||
blobId = key;
|
||||
break;
|
||||
}
|
||||
embededURLSplit.shift();
|
||||
}
|
||||
const value = assets.getAssets().get(blobId);
|
||||
if (value) {
|
||||
name = getAssetName(assets.getAssets(), blobId);
|
||||
size = value.size;
|
||||
type = value.type;
|
||||
}
|
||||
} else {
|
||||
const res = await fetch(embededURL).catch(error => {
|
||||
console.warn('Error fetching embed:', error);
|
||||
return null;
|
||||
});
|
||||
if (!res) {
|
||||
return;
|
||||
}
|
||||
const resCloned = res.clone();
|
||||
name =
|
||||
getFilenameFromContentDisposition(
|
||||
res.headers.get('Content-Disposition') ?? ''
|
||||
) ??
|
||||
(embededURL.split('/').at(-1) ?? 'file') +
|
||||
'.' +
|
||||
(res.headers.get('Content-Type')?.split('/').at(-1) ?? 'blob');
|
||||
const file = new File([await res.blob()], name, {
|
||||
type: res.headers.get('Content-Type') ?? '',
|
||||
});
|
||||
size = file.size;
|
||||
type = file.type;
|
||||
blobId = await sha(await resCloned.arrayBuffer());
|
||||
assets?.getAssets().set(blobId, file);
|
||||
await assets?.writeToBlob(blobId);
|
||||
}
|
||||
walkerContext
|
||||
.openNode(
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: AttachmentBlockSchema.model.flavour,
|
||||
props: {
|
||||
name,
|
||||
size,
|
||||
type,
|
||||
sourceId: blobId,
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.closeNode();
|
||||
walkerContext.skipAllChildren();
|
||||
}
|
||||
},
|
||||
},
|
||||
fromBlockSnapshot: {},
|
||||
};
|
||||
|
||||
export const AttachmentBlockNotionHtmlAdapterExtension =
|
||||
BlockNotionHtmlAdapterExtension(attachmentBlockNotionHtmlAdapterMatcher);
|
||||
@@ -1,299 +0,0 @@
|
||||
import { getEmbedCardIcons } from '@blocksuite/affine-block-embed';
|
||||
import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
|
||||
import { HoverController } from '@blocksuite/affine-components/hover';
|
||||
import {
|
||||
AttachmentIcon16,
|
||||
getAttachmentFileIcons,
|
||||
} from '@blocksuite/affine-components/icons';
|
||||
import { Peekable } from '@blocksuite/affine-components/peek';
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import {
|
||||
type AttachmentBlockModel,
|
||||
AttachmentBlockStyles,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { humanFileSize } from '@blocksuite/affine-shared/utils';
|
||||
import { Slice } from '@blocksuite/store';
|
||||
import { flip, offset } from '@floating-ui/dom';
|
||||
import { html, nothing } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ref } from 'lit/directives/ref.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import type { AttachmentBlockService } from './attachment-service.js';
|
||||
import { AttachmentOptionsTemplate } from './components/options.js';
|
||||
import { AttachmentEmbedProvider } from './embed.js';
|
||||
import { styles } from './styles.js';
|
||||
import { checkAttachmentBlob, downloadAttachmentBlob } from './utils.js';
|
||||
|
||||
@Peekable()
|
||||
export class AttachmentBlockComponent extends CaptionedBlockComponent<
|
||||
AttachmentBlockModel,
|
||||
AttachmentBlockService
|
||||
> {
|
||||
static override styles = styles;
|
||||
|
||||
protected _isDragging = false;
|
||||
|
||||
protected _isResizing = false;
|
||||
|
||||
protected _isSelected = false;
|
||||
|
||||
protected _whenHover: HoverController | null = new HoverController(
|
||||
this,
|
||||
({ abortController }) => {
|
||||
const selection = this.host.selection;
|
||||
const textSelection = selection.find('text');
|
||||
if (
|
||||
!!textSelection &&
|
||||
(!!textSelection.to || !!textSelection.from.length)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const blockSelections = selection.filter('block');
|
||||
if (
|
||||
blockSelections.length > 1 ||
|
||||
(blockSelections.length === 1 &&
|
||||
blockSelections[0].blockId !== this.blockId)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
template: AttachmentOptionsTemplate({
|
||||
block: this,
|
||||
model: this.model,
|
||||
abortController,
|
||||
}),
|
||||
computePosition: {
|
||||
referenceElement: this,
|
||||
placement: 'top-start',
|
||||
middleware: [flip(), offset(4)],
|
||||
autoUpdate: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
blockDraggable = true;
|
||||
|
||||
protected containerStyleMap = styleMap({
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
margin: '18px 0px',
|
||||
});
|
||||
|
||||
convertTo = () => {
|
||||
return this.std
|
||||
.get(AttachmentEmbedProvider)
|
||||
.convertTo(this.model, this.service.maxFileSize);
|
||||
};
|
||||
|
||||
copy = () => {
|
||||
const slice = Slice.fromModels(this.doc, [this.model]);
|
||||
this.std.clipboard.copySlice(slice).catch(console.error);
|
||||
toast(this.host, 'Copied to clipboard');
|
||||
};
|
||||
|
||||
download = () => {
|
||||
downloadAttachmentBlob(this);
|
||||
};
|
||||
|
||||
embedded = () => {
|
||||
return this.std
|
||||
.get(AttachmentEmbedProvider)
|
||||
.embedded(this.model, this.service.maxFileSize);
|
||||
};
|
||||
|
||||
open = () => {
|
||||
if (!this.blobUrl) {
|
||||
return;
|
||||
}
|
||||
window.open(this.blobUrl, '_blank');
|
||||
};
|
||||
|
||||
refreshData = () => {
|
||||
checkAttachmentBlob(this).catch(console.error);
|
||||
};
|
||||
|
||||
protected get embedView() {
|
||||
return this.std
|
||||
.get(AttachmentEmbedProvider)
|
||||
.render(this.model, this.blobUrl, this.service.maxFileSize);
|
||||
}
|
||||
|
||||
private _selectBlock() {
|
||||
const selectionManager = this.host.selection;
|
||||
const blockSelection = selectionManager.create('block', {
|
||||
blockId: this.blockId,
|
||||
});
|
||||
selectionManager.setGroup('note', [blockSelection]);
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.refreshData();
|
||||
|
||||
this.contentEditable = 'false';
|
||||
|
||||
if (!this.model.style) {
|
||||
this.doc.withoutTransact(() => {
|
||||
this.doc.updateBlock(this.model, {
|
||||
style: AttachmentBlockStyles[1],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.model.propsUpdated.on(({ 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())
|
||||
);
|
||||
|
||||
// this is required to prevent iframe from capturing pointer events
|
||||
this.disposables.add(
|
||||
this.std.selection.slots.changed.on(() => {
|
||||
this._isSelected =
|
||||
!!this.selected?.is('block') || !!this.selected?.is('surface');
|
||||
|
||||
this._showOverlay =
|
||||
this._isResizing || this._isDragging || !this._isSelected;
|
||||
})
|
||||
);
|
||||
// this is required to prevent iframe from capturing pointer events
|
||||
this.handleEvent('dragStart', () => {
|
||||
this._isDragging = true;
|
||||
this._showOverlay =
|
||||
this._isResizing || this._isDragging || !this._isSelected;
|
||||
});
|
||||
|
||||
this.handleEvent('dragEnd', () => {
|
||||
this._isDragging = false;
|
||||
this._showOverlay =
|
||||
this._isResizing || this._isDragging || !this._isSelected;
|
||||
});
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
if (this.blobUrl) {
|
||||
URL.revokeObjectURL(this.blobUrl);
|
||||
}
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
// lazy bindings
|
||||
this.disposables.addFromEvent(this, 'click', this.onClick);
|
||||
}
|
||||
|
||||
protected onClick(event: MouseEvent) {
|
||||
// the peek view need handle shift + click
|
||||
if (event.defaultPrevented) return;
|
||||
|
||||
event.stopPropagation();
|
||||
|
||||
this._selectBlock();
|
||||
}
|
||||
|
||||
override renderBlock() {
|
||||
const { name, size, style } = this.model;
|
||||
const cardStyle = style ?? AttachmentBlockStyles[1];
|
||||
|
||||
const theme = this.std.get(ThemeProvider).theme;
|
||||
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 fileType = name.split('.').pop() ?? '';
|
||||
const FileTypeIcon = getAttachmentFileIcons(fileType);
|
||||
|
||||
const embedView = this.embedView;
|
||||
|
||||
return html`
|
||||
<div
|
||||
${this._whenHover ? ref(this._whenHover.setReference) : nothing}
|
||||
class="affine-attachment-container"
|
||||
draggable="${this.blockDraggable ? 'true' : 'false'}"
|
||||
style=${this.containerStyleMap}
|
||||
>
|
||||
${embedView
|
||||
? html`<div class="affine-attachment-embed-container">
|
||||
${embedView}
|
||||
|
||||
<div
|
||||
class=${classMap({
|
||||
'affine-attachment-iframe-overlay': true,
|
||||
hide: !this._showOverlay,
|
||||
})}
|
||||
></div>
|
||||
</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>
|
||||
`;
|
||||
}
|
||||
|
||||
@state()
|
||||
protected accessor _showOverlay = true;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor allowEmbed = false;
|
||||
|
||||
@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 useCaptionEditor = true;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-attachment': AttachmentBlockComponent;
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import type { HoverController } from '@blocksuite/affine-components/hover';
|
||||
import { AttachmentBlockStyles } from '@blocksuite/affine-model';
|
||||
import {
|
||||
EMBED_CARD_HEIGHT,
|
||||
EMBED_CARD_WIDTH,
|
||||
} from '@blocksuite/affine-shared/consts';
|
||||
import { toGfxBlockComponent } from '@blocksuite/block-std';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import type { EdgelessRootService } from '../root-block/index.js';
|
||||
import { AttachmentBlockComponent } from './attachment-block.js';
|
||||
|
||||
export class AttachmentEdgelessBlockComponent extends toGfxBlockComponent(
|
||||
AttachmentBlockComponent
|
||||
) {
|
||||
protected override _whenHover: HoverController | null = null;
|
||||
|
||||
override blockDraggable = false;
|
||||
|
||||
get rootService() {
|
||||
return this.std.getService('affine:page') as EdgelessRootService;
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
const rootService = this.rootService;
|
||||
|
||||
this._disposables.add(
|
||||
rootService.slots.elementResizeStart.on(() => {
|
||||
this._isResizing = true;
|
||||
this._showOverlay = true;
|
||||
})
|
||||
);
|
||||
|
||||
this._disposables.add(
|
||||
rootService.slots.elementResizeEnd.on(() => {
|
||||
this._isResizing = false;
|
||||
this._showOverlay =
|
||||
this._isResizing || this._isDragging || !this._isSelected;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override onClick(_: MouseEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
override renderGfxBlock() {
|
||||
const { style$ } = this.model;
|
||||
const cardStyle = style$.value ?? AttachmentBlockStyles[1];
|
||||
const width = EMBED_CARD_WIDTH[cardStyle];
|
||||
const height = EMBED_CARD_HEIGHT[cardStyle];
|
||||
const bound = this.model.elementBound;
|
||||
const scaleX = bound.w / width;
|
||||
const scaleY = bound.h / height;
|
||||
|
||||
this.containerStyleMap = styleMap({
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
transform: `scale(${scaleX}, ${scaleY})`,
|
||||
transformOrigin: '0 0',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
return this.renderPageContent();
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-edgeless-attachment': AttachmentEdgelessBlockComponent;
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { FileDropConfigExtension } from '@blocksuite/affine-components/drag-indicator';
|
||||
import { AttachmentBlockSchema } from '@blocksuite/affine-model';
|
||||
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
isInsideEdgelessEditor,
|
||||
matchFlavours,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { BlockService } from '@blocksuite/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
|
||||
import { addAttachments } from '../root-block/edgeless/utils/common.js';
|
||||
import { addSiblingAttachmentBlocks } from './utils.js';
|
||||
|
||||
// bytes.parse('2GB')
|
||||
const maxFileSize = 2147483648;
|
||||
|
||||
export class AttachmentBlockService extends BlockService {
|
||||
static override readonly flavour = AttachmentBlockSchema.model.flavour;
|
||||
|
||||
maxFileSize = maxFileSize;
|
||||
}
|
||||
|
||||
export const AttachmentDropOption = FileDropConfigExtension({
|
||||
flavour: AttachmentBlockSchema.model.flavour,
|
||||
onDrop: ({ files, targetModel, place, point, std }) => {
|
||||
// generic attachment block for all files except images
|
||||
const attachmentFiles = files.filter(
|
||||
file => !file.type.startsWith('image/')
|
||||
);
|
||||
|
||||
if (!attachmentFiles.length) return false;
|
||||
|
||||
if (targetModel && !matchFlavours(targetModel, ['affine:surface'])) {
|
||||
addSiblingAttachmentBlocks(
|
||||
std.host,
|
||||
attachmentFiles,
|
||||
// TODO: use max file size from service
|
||||
maxFileSize,
|
||||
targetModel,
|
||||
place
|
||||
).catch(console.error);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isInsideEdgelessEditor(std.host)) {
|
||||
const gfx = std.get(GfxControllerIdentifier);
|
||||
point = gfx.viewport.toViewCoordFromClientCoord(point);
|
||||
addAttachments(std, attachmentFiles, point).catch(console.error);
|
||||
|
||||
std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', {
|
||||
control: 'canvas:drop',
|
||||
page: 'whiteboard editor',
|
||||
module: 'toolbar',
|
||||
segment: 'toolbar',
|
||||
type: 'attachment',
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import {
|
||||
BlockViewExtension,
|
||||
type ExtensionType,
|
||||
FlavourExtension,
|
||||
} from '@blocksuite/block-std';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { AttachmentBlockNotionHtmlAdapterExtension } from './adapters/notion-html.js';
|
||||
import {
|
||||
AttachmentBlockService,
|
||||
AttachmentDropOption,
|
||||
} from './attachment-service.js';
|
||||
import {
|
||||
AttachmentEmbedConfigExtension,
|
||||
AttachmentEmbedService,
|
||||
} from './embed.js';
|
||||
|
||||
export const AttachmentBlockSpec: ExtensionType[] = [
|
||||
FlavourExtension('affine:attachment'),
|
||||
AttachmentBlockService,
|
||||
BlockViewExtension('affine:attachment', model => {
|
||||
return model.parent?.flavour === 'affine:surface'
|
||||
? literal`affine-edgeless-attachment`
|
||||
: literal`affine-attachment`;
|
||||
}),
|
||||
AttachmentDropOption,
|
||||
AttachmentEmbedConfigExtension(),
|
||||
AttachmentEmbedService,
|
||||
AttachmentBlockNotionHtmlAdapterExtension,
|
||||
];
|
||||
@@ -1,77 +0,0 @@
|
||||
import {
|
||||
CopyIcon,
|
||||
DeleteIcon,
|
||||
DownloadIcon,
|
||||
DuplicateIcon,
|
||||
RefreshIcon,
|
||||
} from '@blocksuite/affine-components/icons';
|
||||
import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar';
|
||||
|
||||
import { cloneAttachmentProperties } from '../utils.js';
|
||||
import type { AttachmentToolbarMoreMenuContext } from './context.js';
|
||||
|
||||
export const BUILT_IN_GROUPS: MenuItemGroup<AttachmentToolbarMoreMenuContext>[] =
|
||||
[
|
||||
{
|
||||
type: 'clipboard',
|
||||
items: [
|
||||
{
|
||||
type: 'copy',
|
||||
label: 'Copy',
|
||||
icon: CopyIcon,
|
||||
disabled: ({ doc }) => doc.readonly,
|
||||
action: ctx => ctx.blockComponent.copy(),
|
||||
},
|
||||
{
|
||||
type: 'duplicate',
|
||||
label: 'Duplicate',
|
||||
icon: DuplicateIcon,
|
||||
disabled: ({ doc }) => doc.readonly,
|
||||
action: ({ doc, blockComponent, close }) => {
|
||||
const model = blockComponent.model;
|
||||
const prop: { flavour: 'affine:attachment' } = {
|
||||
flavour: 'affine:attachment',
|
||||
...cloneAttachmentProperties(model),
|
||||
};
|
||||
doc.addSiblingBlocks(model, [prop]);
|
||||
close();
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'reload',
|
||||
label: 'Reload',
|
||||
icon: RefreshIcon,
|
||||
disabled: ({ doc }) => doc.readonly,
|
||||
action: ({ blockComponent, close }) => {
|
||||
blockComponent.refreshData();
|
||||
close();
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'download',
|
||||
label: 'Download',
|
||||
icon: DownloadIcon,
|
||||
disabled: ({ doc }) => doc.readonly,
|
||||
action: ({ blockComponent, close }) => {
|
||||
blockComponent.download();
|
||||
close();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'delete',
|
||||
items: [
|
||||
{
|
||||
type: 'delete',
|
||||
label: 'Delete',
|
||||
icon: DeleteIcon,
|
||||
disabled: ({ doc }) => doc.readonly,
|
||||
action: ({ doc, blockComponent, close }) => {
|
||||
doc.deleteBlock(blockComponent.model);
|
||||
close();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -1,45 +0,0 @@
|
||||
import { MenuContext } from '@blocksuite/affine-components/toolbar';
|
||||
|
||||
import type { AttachmentBlockComponent } from '../attachment-block.js';
|
||||
|
||||
export class AttachmentToolbarMoreMenuContext extends MenuContext {
|
||||
override close = () => {
|
||||
this.abortController.abort();
|
||||
};
|
||||
|
||||
get doc() {
|
||||
return this.blockComponent.doc;
|
||||
}
|
||||
|
||||
get host() {
|
||||
return this.blockComponent.host;
|
||||
}
|
||||
|
||||
get selectedBlockModels() {
|
||||
if (this.blockComponent.model) return [this.blockComponent.model];
|
||||
return [];
|
||||
}
|
||||
|
||||
get std() {
|
||||
return this.blockComponent.std;
|
||||
}
|
||||
|
||||
constructor(
|
||||
public blockComponent: AttachmentBlockComponent,
|
||||
public abortController: AbortController
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
isEmpty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isMultiple() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isSingle() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
import {
|
||||
CaptionIcon,
|
||||
DownloadIcon,
|
||||
EditIcon,
|
||||
MoreVerticalIcon,
|
||||
SmallArrowDownIcon,
|
||||
} from '@blocksuite/affine-components/icons';
|
||||
import { createLitPortal } from '@blocksuite/affine-components/portal';
|
||||
import {
|
||||
cloneGroups,
|
||||
getMoreMenuConfig,
|
||||
renderGroups,
|
||||
renderToolbarSeparator,
|
||||
} from '@blocksuite/affine-components/toolbar';
|
||||
import {
|
||||
type AttachmentBlockModel,
|
||||
defaultAttachmentProps,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
EMBED_CARD_HEIGHT,
|
||||
EMBED_CARD_WIDTH,
|
||||
} from '@blocksuite/affine-shared/consts';
|
||||
import { Bound } from '@blocksuite/global/utils';
|
||||
import { flip, offset } from '@floating-ui/dom';
|
||||
import { html, nothing } from 'lit';
|
||||
import { join } from 'lit/directives/join.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import type { AttachmentBlockComponent } from '../attachment-block.js';
|
||||
import { BUILT_IN_GROUPS } from './config.js';
|
||||
import { AttachmentToolbarMoreMenuContext } from './context.js';
|
||||
import { RenameModal } from './rename-model.js';
|
||||
import { styles } from './styles.js';
|
||||
|
||||
export function attachmentViewToggleMenu({
|
||||
block,
|
||||
callback,
|
||||
}: {
|
||||
block: AttachmentBlockComponent;
|
||||
callback?: () => void;
|
||||
}) {
|
||||
const model = block.model;
|
||||
const readonly = model.doc.readonly;
|
||||
const embedded = model.embed;
|
||||
const viewType = embedded ? 'embed' : 'card';
|
||||
const viewActions = [
|
||||
{
|
||||
type: 'card',
|
||||
label: 'Card view',
|
||||
disabled: readonly || !embedded,
|
||||
action: () => {
|
||||
const style = defaultAttachmentProps.style!;
|
||||
const width = EMBED_CARD_WIDTH[style];
|
||||
const height = EMBED_CARD_HEIGHT[style];
|
||||
const bound = Bound.deserialize(model.xywh);
|
||||
bound.w = width;
|
||||
bound.h = height;
|
||||
model.doc.updateBlock(model, {
|
||||
style,
|
||||
embed: false,
|
||||
xywh: bound.serialize(),
|
||||
});
|
||||
callback?.();
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'embed',
|
||||
label: 'Embed view',
|
||||
disabled: readonly || embedded || !block.embedded(),
|
||||
action: () => {
|
||||
block.convertTo();
|
||||
callback?.();
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<editor-menu-button
|
||||
.contentPadding=${'8px'}
|
||||
.button=${html`
|
||||
<editor-icon-button
|
||||
aria-label="Switch view"
|
||||
.justify=${'space-between'}
|
||||
.labelHeight=${'20px'}
|
||||
.iconContainerWidth=${'110px'}
|
||||
>
|
||||
<div class="label">
|
||||
<span style="text-transform: capitalize">${viewType}</span>
|
||||
view
|
||||
</div>
|
||||
${SmallArrowDownIcon}
|
||||
</editor-icon-button>
|
||||
`}
|
||||
>
|
||||
<div data-size="small" data-orientation="vertical">
|
||||
${repeat(
|
||||
viewActions,
|
||||
button => button.type,
|
||||
({ type, label, action, disabled }) => html`
|
||||
<editor-menu-action
|
||||
data-testid=${`link-to-${type}`}
|
||||
?data-selected=${type === viewType}
|
||||
?disabled=${disabled}
|
||||
@click=${action}
|
||||
>
|
||||
${label}
|
||||
</editor-menu-action>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</editor-menu-button>
|
||||
`;
|
||||
}
|
||||
|
||||
export function AttachmentOptionsTemplate({
|
||||
block,
|
||||
model,
|
||||
abortController,
|
||||
}: {
|
||||
block: AttachmentBlockComponent;
|
||||
model: AttachmentBlockModel;
|
||||
abortController: AbortController;
|
||||
}) {
|
||||
const std = block.std;
|
||||
const editorHost = block.host;
|
||||
const readonly = model.doc.readonly;
|
||||
const context = new AttachmentToolbarMoreMenuContext(block, abortController);
|
||||
const groups = getMoreMenuConfig(std).configure(cloneGroups(BUILT_IN_GROUPS));
|
||||
const moreMenuActions = renderGroups(groups, context);
|
||||
|
||||
const buttons = [
|
||||
// preview
|
||||
// html`
|
||||
// <editor-icon-button aria-label="Preview" .tooltip=${'Preview'}>
|
||||
// ${ViewIcon}
|
||||
// </editor-icon-button>
|
||||
// `,
|
||||
|
||||
readonly
|
||||
? nothing
|
||||
: html`
|
||||
<editor-icon-button
|
||||
aria-label="Rename"
|
||||
.tooltip=${'Rename'}
|
||||
@click=${() => {
|
||||
abortController.abort();
|
||||
const renameAbortController = new AbortController();
|
||||
createLitPortal({
|
||||
template: RenameModal({
|
||||
model,
|
||||
editorHost,
|
||||
abortController: renameAbortController,
|
||||
}),
|
||||
computePosition: {
|
||||
referenceElement: block,
|
||||
placement: 'top-start',
|
||||
middleware: [flip(), offset(4)],
|
||||
// It has a overlay mask, so we don't need to update the position.
|
||||
// autoUpdate: true,
|
||||
},
|
||||
abortController: renameAbortController,
|
||||
});
|
||||
}}
|
||||
>
|
||||
${EditIcon}
|
||||
</editor-icon-button>
|
||||
`,
|
||||
|
||||
attachmentViewToggleMenu({
|
||||
block,
|
||||
callback: () => abortController.abort(),
|
||||
}),
|
||||
|
||||
readonly
|
||||
? nothing
|
||||
: html`
|
||||
<editor-icon-button
|
||||
aria-label="Download"
|
||||
.tooltip=${'Download'}
|
||||
@click=${() => block.download()}
|
||||
>
|
||||
${DownloadIcon}
|
||||
</editor-icon-button>
|
||||
`,
|
||||
|
||||
readonly
|
||||
? nothing
|
||||
: html`
|
||||
<editor-icon-button
|
||||
aria-label="Caption"
|
||||
.tooltip=${'Caption'}
|
||||
@click=${() => block.captionEditor?.show()}
|
||||
>
|
||||
${CaptionIcon}
|
||||
</editor-icon-button>
|
||||
`,
|
||||
|
||||
html`
|
||||
<editor-menu-button
|
||||
.contentPadding=${'8px'}
|
||||
.button=${html`
|
||||
<editor-icon-button aria-label="More" .tooltip=${'More'}>
|
||||
${MoreVerticalIcon}
|
||||
</editor-icon-button>
|
||||
`}
|
||||
>
|
||||
<div data-size="large" data-orientation="vertical">
|
||||
${moreMenuActions}
|
||||
</div>
|
||||
</editor-menu-button>
|
||||
`,
|
||||
];
|
||||
|
||||
return html`
|
||||
<style>
|
||||
${styles}
|
||||
</style>
|
||||
<editor-toolbar class="affine-attachment-toolbar">
|
||||
${join(
|
||||
buttons.filter(button => button !== nothing),
|
||||
renderToolbarSeparator
|
||||
)}
|
||||
</editor-toolbar>
|
||||
`;
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import { ConfirmIcon } from '@blocksuite/affine-components/icons';
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import type { AttachmentBlockModel } from '@blocksuite/affine-model';
|
||||
import type { EditorHost } from '@blocksuite/block-std';
|
||||
import { html } from 'lit';
|
||||
import { createRef, ref } from 'lit/directives/ref.js';
|
||||
|
||||
import { renameStyles } from './styles.js';
|
||||
|
||||
export const RenameModal = ({
|
||||
editorHost,
|
||||
model,
|
||||
abortController,
|
||||
}: {
|
||||
editorHost: EditorHost;
|
||||
model: AttachmentBlockModel;
|
||||
abortController: AbortController;
|
||||
}) => {
|
||||
const inputRef = createRef<HTMLInputElement>();
|
||||
// Fix auto focus
|
||||
setTimeout(() => inputRef.value?.focus());
|
||||
const originalName = model.name;
|
||||
const nameWithoutExtension = originalName.slice(
|
||||
0,
|
||||
originalName.lastIndexOf('.')
|
||||
);
|
||||
const originalExtension = originalName.slice(originalName.lastIndexOf('.'));
|
||||
const includeExtension =
|
||||
originalExtension.includes('.') &&
|
||||
originalExtension.length <= 7 &&
|
||||
// including the dot
|
||||
originalName.length > originalExtension.length;
|
||||
|
||||
let fileName = includeExtension ? nameWithoutExtension : originalName;
|
||||
const extension = includeExtension ? originalExtension : '';
|
||||
|
||||
const onConfirm = () => {
|
||||
const newFileName = fileName + extension;
|
||||
if (!newFileName) {
|
||||
toast(editorHost, 'File name cannot be empty');
|
||||
return;
|
||||
}
|
||||
model.doc.updateBlock(model, {
|
||||
name: newFileName,
|
||||
});
|
||||
abortController.abort();
|
||||
};
|
||||
const onInput = (e: InputEvent) => {
|
||||
fileName = (e.target as HTMLInputElement).value;
|
||||
};
|
||||
const onKeydown = (e: KeyboardEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (e.key === 'Escape' && !e.isComposing) {
|
||||
abortController.abort();
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Enter' && !e.isComposing) {
|
||||
onConfirm();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return html`
|
||||
<style>
|
||||
${renameStyles}
|
||||
</style>
|
||||
<div
|
||||
class="affine-attachment-rename-overlay-mask"
|
||||
@click="${() => abortController.abort()}"
|
||||
></div>
|
||||
<div class="affine-attachment-rename-container">
|
||||
<div class="affine-attachment-rename-input-wrapper">
|
||||
<input
|
||||
${ref(inputRef)}
|
||||
type="text"
|
||||
.value=${fileName}
|
||||
@input=${onInput}
|
||||
@keydown=${onKeydown}
|
||||
/>
|
||||
<span class="affine-attachment-rename-extension">${extension}</span>
|
||||
</div>
|
||||
<editor-icon-button
|
||||
class="affine-confirm-button"
|
||||
.iconSize=${'24px'}
|
||||
@click=${onConfirm}
|
||||
>
|
||||
${ConfirmIcon}
|
||||
</editor-icon-button>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
@@ -1,101 +0,0 @@
|
||||
import { FONT_XS, PANEL_BASE } from '@blocksuite/affine-shared/styles';
|
||||
import { css } from 'lit';
|
||||
|
||||
export const renameStyles = css`
|
||||
.affine-attachment-rename-container {
|
||||
${PANEL_BASE};
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 320px;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
z-index: var(--affine-z-index-popover);
|
||||
}
|
||||
|
||||
.affine-attachment-rename-input-wrapper {
|
||||
display: flex;
|
||||
min-width: 280px;
|
||||
height: 30px;
|
||||
box-sizing: border-box;
|
||||
padding: 4px 10px;
|
||||
background: var(--affine-white-10);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--affine-border-color);
|
||||
}
|
||||
|
||||
.affine-attachment-rename-input-wrapper:focus-within {
|
||||
border-color: var(--affine-blue-700);
|
||||
box-shadow: var(--affine-active-shadow);
|
||||
}
|
||||
|
||||
.affine-attachment-rename-input-wrapper input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
color: var(--affine-text-primary-color);
|
||||
${FONT_XS};
|
||||
}
|
||||
|
||||
.affine-attachment-rename-input-wrapper input::placeholder {
|
||||
color: var(--affine-placeholder-color);
|
||||
}
|
||||
|
||||
.affine-attachment-rename-extension {
|
||||
font-size: var(--affine-font-xs);
|
||||
color: var(--affine-text-secondary-color);
|
||||
}
|
||||
|
||||
.affine-attachment-rename-overlay-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: var(--affine-z-index-popover);
|
||||
}
|
||||
`;
|
||||
|
||||
export const moreMenuStyles = css`
|
||||
.affine-attachment-options-more {
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.affine-attachment-options-more-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: var(--affine-text-primary-color);
|
||||
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
background: var(--affine-background-overlay-panel-color);
|
||||
box-shadow: var(--affine-shadow-2);
|
||||
}
|
||||
|
||||
.affine-attachment-options-more-container > icon-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
.affine-attachment-options-more-container > icon-button[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.affine-attachment-options-more-container > icon-button:hover.danger {
|
||||
background: var(--affine-background-error-color);
|
||||
color: var(--affine-error-color);
|
||||
}
|
||||
.affine-attachment-options-more-container > icon-button:hover.danger > svg {
|
||||
color: var(--affine-error-color);
|
||||
}
|
||||
`;
|
||||
|
||||
export const styles = css`
|
||||
:host {
|
||||
z-index: 1;
|
||||
}
|
||||
`;
|
||||
@@ -1,192 +0,0 @@
|
||||
import type {
|
||||
AttachmentBlockModel,
|
||||
ImageBlockProps,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
transformModel,
|
||||
withTempBlobData,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
import { Extension } from '@blocksuite/block-std';
|
||||
import type { Container } from '@blocksuite/global/di';
|
||||
import { createIdentifier } from '@blocksuite/global/di';
|
||||
import type { TemplateResult } from 'lit';
|
||||
import { html } from 'lit';
|
||||
|
||||
export type AttachmentEmbedConfig = {
|
||||
name: string;
|
||||
/**
|
||||
* Check if the attachment can be turned into embed view.
|
||||
*/
|
||||
check: (model: AttachmentBlockModel, maxFileSize: number) => boolean;
|
||||
/**
|
||||
* The action will be executed when the 「Turn into embed view」 button is clicked.
|
||||
*/
|
||||
action?: (model: AttachmentBlockModel) => Promise<void> | void;
|
||||
/**
|
||||
* The template will be used to render the embed view.
|
||||
*/
|
||||
template?: (model: AttachmentBlockModel, blobUrl: string) => TemplateResult;
|
||||
};
|
||||
|
||||
// Single embed config.
|
||||
export const AttachmentEmbedConfigIdentifier =
|
||||
createIdentifier<AttachmentEmbedConfig>(
|
||||
'AffineAttachmentEmbedConfigIdentifier'
|
||||
);
|
||||
|
||||
export function AttachmentEmbedConfigExtension(
|
||||
configs: AttachmentEmbedConfig[] = embedConfig
|
||||
): ExtensionType {
|
||||
return {
|
||||
setup: di => {
|
||||
configs.forEach(option => {
|
||||
di.addImpl(AttachmentEmbedConfigIdentifier(option.name), () => option);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// A embed config map.
|
||||
export const AttachmentEmbedConfigMapIdentifier = createIdentifier<
|
||||
Map<string, AttachmentEmbedConfig>
|
||||
>('AffineAttachmentEmbedConfigMapIdentifier');
|
||||
|
||||
export const AttachmentEmbedProvider = createIdentifier<AttachmentEmbedService>(
|
||||
'AffineAttachmentEmbedProvider'
|
||||
);
|
||||
|
||||
export class AttachmentEmbedService extends Extension {
|
||||
// 10MB
|
||||
static MAX_EMBED_SIZE = 10 * 1024 * 1024;
|
||||
|
||||
get keys() {
|
||||
return this.configs.keys();
|
||||
}
|
||||
|
||||
get values() {
|
||||
return this.configs.values();
|
||||
}
|
||||
|
||||
constructor(private readonly configs: Map<string, AttachmentEmbedConfig>) {
|
||||
super();
|
||||
}
|
||||
|
||||
static override setup(di: Container) {
|
||||
di.addImpl(AttachmentEmbedConfigMapIdentifier, provider =>
|
||||
provider.getAll(AttachmentEmbedConfigIdentifier)
|
||||
);
|
||||
di.addImpl(AttachmentEmbedProvider, AttachmentEmbedService, [
|
||||
AttachmentEmbedConfigMapIdentifier,
|
||||
]);
|
||||
}
|
||||
|
||||
// Converts to embed view.
|
||||
convertTo(
|
||||
model: AttachmentBlockModel,
|
||||
maxFileSize = AttachmentEmbedService.MAX_EMBED_SIZE
|
||||
) {
|
||||
const config = this.values.find(config => config.check(model, maxFileSize));
|
||||
if (!config || !config.action) {
|
||||
model.doc.updateBlock(model, { embed: true });
|
||||
return;
|
||||
}
|
||||
config.action(model)?.catch(console.error);
|
||||
}
|
||||
|
||||
embedded(
|
||||
model: AttachmentBlockModel,
|
||||
maxFileSize = AttachmentEmbedService.MAX_EMBED_SIZE
|
||||
) {
|
||||
return this.values.some(config => config.check(model, maxFileSize));
|
||||
}
|
||||
|
||||
render(
|
||||
model: AttachmentBlockModel,
|
||||
blobUrl?: string,
|
||||
maxFileSize = AttachmentEmbedService.MAX_EMBED_SIZE
|
||||
) {
|
||||
if (!model.embed || !blobUrl) return;
|
||||
|
||||
const config = this.values.find(config => config.check(model, maxFileSize));
|
||||
if (!config || !config.template) {
|
||||
console.error('No embed view template found!', model, model.type);
|
||||
return;
|
||||
}
|
||||
|
||||
return config.template(model, blobUrl);
|
||||
}
|
||||
}
|
||||
|
||||
const embedConfig: AttachmentEmbedConfig[] = [
|
||||
{
|
||||
name: 'image',
|
||||
check: model =>
|
||||
model.doc.schema.flavourSchemaMap.has('affine:image') &&
|
||||
model.type.startsWith('image/'),
|
||||
action: model => turnIntoImageBlock(model),
|
||||
},
|
||||
{
|
||||
name: 'pdf',
|
||||
check: (model, maxFileSize) =>
|
||||
model.type === 'application/pdf' && model.size <= maxFileSize,
|
||||
template: (_, blobUrl) => {
|
||||
// More options: https://tinytip.co/tips/html-pdf-params/
|
||||
// https://chromium.googlesource.com/chromium/src/+/refs/tags/121.0.6153.1/chrome/browser/resources/pdf/open_pdf_params_parser.ts
|
||||
const parameters = '#toolbar=0';
|
||||
return html`<iframe
|
||||
style="width: 100%; color-scheme: auto;"
|
||||
height="480"
|
||||
src=${blobUrl + parameters}
|
||||
loading="lazy"
|
||||
scrolling="no"
|
||||
frameborder="no"
|
||||
allowTransparency
|
||||
allowfullscreen
|
||||
type="application/pdf"
|
||||
></iframe>`;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'video',
|
||||
check: (model, maxFileSize) =>
|
||||
model.type.startsWith('video/') && model.size <= maxFileSize,
|
||||
template: (_, blobUrl) =>
|
||||
html`<video width="100%;" height="480" controls src=${blobUrl}></video>`,
|
||||
},
|
||||
{
|
||||
name: 'audio',
|
||||
check: (model, maxFileSize) =>
|
||||
model.type.startsWith('audio/') && model.size <= maxFileSize,
|
||||
template: (_, blobUrl) =>
|
||||
html`<audio controls src=${blobUrl} style="margin: 4px;"></audio>`,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Turn the attachment block into an image block.
|
||||
*/
|
||||
export function turnIntoImageBlock(model: AttachmentBlockModel) {
|
||||
if (!model.doc.schema.flavourSchemaMap.has('affine:image')) {
|
||||
console.error('The image flavour is not supported!');
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceId = model.sourceId;
|
||||
if (!sourceId) return;
|
||||
|
||||
const { saveAttachmentData, getImageData } = withTempBlobData();
|
||||
saveAttachmentData(sourceId, { name: model.name });
|
||||
|
||||
const imageConvertData = model.sourceId
|
||||
? getImageData(model.sourceId)
|
||||
: undefined;
|
||||
|
||||
const imageProp: Partial<ImageBlockProps> = {
|
||||
sourceId,
|
||||
caption: model.caption,
|
||||
size: model.size,
|
||||
...imageConvertData,
|
||||
};
|
||||
transformModel(model, 'affine:image', imageProp);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export * from './attachment-block.js';
|
||||
export * from './attachment-service.js';
|
||||
export { attachmentViewToggleMenu } from './components/options.js';
|
||||
export {
|
||||
type AttachmentEmbedConfig,
|
||||
AttachmentEmbedConfigIdentifier,
|
||||
AttachmentEmbedProvider,
|
||||
} from './embed.js';
|
||||
@@ -1,158 +0,0 @@
|
||||
import {
|
||||
EMBED_CARD_HEIGHT,
|
||||
EMBED_CARD_WIDTH,
|
||||
} from '@blocksuite/affine-shared/consts';
|
||||
import { css } from 'lit';
|
||||
|
||||
export const styles = css`
|
||||
.affine-attachment-card {
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
width: 100%;
|
||||
height: ${EMBED_CARD_HEIGHT.horizontalThin}px;
|
||||
|
||||
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;
|
||||
|
||||
border-radius: var(--1, 0px);
|
||||
opacity: var(--add, 1);
|
||||
}
|
||||
|
||||
.affine-attachment-content-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
align-self: stretch;
|
||||
padding: var(--1, 0px);
|
||||
border-radius: var(--1, 0px);
|
||||
opacity: var(--add, 1);
|
||||
}
|
||||
|
||||
.affine-attachment-content-title-icon {
|
||||
display: flex;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.affine-attachment-content-title-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: var(--affine-background-primary-color);
|
||||
}
|
||||
|
||||
.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;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.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;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.affine-attachment-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.affine-attachment-banner svg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.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.cubeThick {
|
||||
width: ${EMBED_CARD_WIDTH.cubeThick}px;
|
||||
height: ${EMBED_CARD_HEIGHT.cubeThick}px;
|
||||
|
||||
flex-direction: column-reverse;
|
||||
|
||||
.affine-attachment-content {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.affine-attachment-banner {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.affine-attachment-embed-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.affine-attachment-iframe-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.affine-attachment-iframe-overlay.hide {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
@@ -1,253 +0,0 @@
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import type {
|
||||
AttachmentBlockModel,
|
||||
AttachmentBlockProps,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { defaultAttachmentProps } from '@blocksuite/affine-model';
|
||||
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
||||
import { humanFileSize } from '@blocksuite/affine-shared/utils';
|
||||
import type { EditorHost } from '@blocksuite/block-std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import type { AttachmentBlockComponent } from './attachment-block.js';
|
||||
|
||||
export function cloneAttachmentProperties(model: AttachmentBlockModel) {
|
||||
const clonedProps = {} as AttachmentBlockProps;
|
||||
for (const cur in defaultAttachmentProps) {
|
||||
const key = cur as keyof AttachmentBlockProps;
|
||||
// @ts-expect-error it's safe because we just cloned the props simply
|
||||
clonedProps[key] = model[
|
||||
key
|
||||
] as AttachmentBlockProps[keyof AttachmentBlockProps];
|
||||
}
|
||||
return clonedProps;
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
export async function uploadAttachmentBlob(
|
||||
editorHost: EditorHost,
|
||||
blockId: string,
|
||||
blob: Blob,
|
||||
filetype: string,
|
||||
isEdgeless?: boolean
|
||||
): Promise<void> {
|
||||
if (isAttachmentUploading(blockId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = editorHost.doc;
|
||||
let sourceId: string | undefined;
|
||||
|
||||
try {
|
||||
setAttachmentUploading(blockId);
|
||||
sourceId = await doc.blobSync.set(blob);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof Error) {
|
||||
toast(
|
||||
editorHost,
|
||||
`Failed to upload attachment! ${error.message || error.toString()}`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setAttachmentUploaded(blockId);
|
||||
|
||||
const block = doc.getBlock(blockId);
|
||||
|
||||
doc.withoutTransact(() => {
|
||||
if (!block) return;
|
||||
|
||||
doc.updateBlock(block.model, {
|
||||
sourceId,
|
||||
} satisfies Partial<AttachmentBlockProps>);
|
||||
});
|
||||
|
||||
editorHost.std
|
||||
.getOptional(TelemetryProvider)
|
||||
?.track('AttachmentUploadedEvent', {
|
||||
page: `${isEdgeless ? 'whiteboard' : 'doc'} editor`,
|
||||
module: 'attachment',
|
||||
segment: 'attachment',
|
||||
control: 'uploader',
|
||||
type: filetype,
|
||||
category: block && sourceId ? 'success' : 'failure',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function getAttachmentBlob(model: AttachmentBlockModel) {
|
||||
const sourceId = model.sourceId;
|
||||
if (!sourceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const doc = model.doc;
|
||||
let blob = await doc.blobSync.get(sourceId);
|
||||
|
||||
if (blob) {
|
||||
blob = new Blob([blob], { type: model.type });
|
||||
}
|
||||
|
||||
return blob;
|
||||
}
|
||||
|
||||
export async function checkAttachmentBlob(block: AttachmentBlockComponent) {
|
||||
const model = block.model;
|
||||
const { id, sourceId } = model;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Since the size of the attachment may be very large,
|
||||
* 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;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
toast(host, 'Please wait, file is loading...');
|
||||
return;
|
||||
}
|
||||
|
||||
const name = model.name;
|
||||
const shortName = name.length < 20 ? name : name.slice(0, 20) + '...';
|
||||
|
||||
if (error || !blobUrl) {
|
||||
toast(host, `Failed to download ${shortName}!`);
|
||||
return;
|
||||
}
|
||||
|
||||
block.downloading = true;
|
||||
|
||||
toast(host, `Downloading ${shortName}`);
|
||||
|
||||
const tmpLink = document.createElement('a');
|
||||
const event = new MouseEvent('click');
|
||||
tmpLink.download = name;
|
||||
tmpLink.href = blobUrl;
|
||||
tmpLink.dispatchEvent(event);
|
||||
tmpLink.remove();
|
||||
|
||||
block.downloading = false;
|
||||
}
|
||||
|
||||
export async function getFileType(file: File) {
|
||||
if (file.type) {
|
||||
return file.type;
|
||||
}
|
||||
// If the file type is not available, try to get it from the buffer.
|
||||
const buffer = await file.arrayBuffer();
|
||||
const FileType = await import('file-type');
|
||||
const fileType = await FileType.fileTypeFromBuffer(buffer);
|
||||
return fileType ? fileType.mime : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new attachment block before / after the specified block.
|
||||
*/
|
||||
export async function addSiblingAttachmentBlocks(
|
||||
editorHost: EditorHost,
|
||||
files: File[],
|
||||
maxFileSize: number,
|
||||
targetModel: BlockModel,
|
||||
place: 'before' | 'after' = 'after'
|
||||
) {
|
||||
if (!files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isSizeExceeded = files.some(file => file.size > maxFileSize);
|
||||
if (isSizeExceeded) {
|
||||
toast(
|
||||
editorHost,
|
||||
`You can only upload files less than ${humanFileSize(
|
||||
maxFileSize,
|
||||
true,
|
||||
0
|
||||
)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = targetModel.doc;
|
||||
|
||||
// Get the types of all files
|
||||
const types = await Promise.all(files.map(file => getFileType(file)));
|
||||
const attachmentBlockProps: (Partial<AttachmentBlockProps> & {
|
||||
flavour: 'affine:attachment';
|
||||
})[] = files.map((file, index) => ({
|
||||
flavour: 'affine:attachment',
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: types[index],
|
||||
}));
|
||||
|
||||
const blockIds = doc.addSiblingBlocks(
|
||||
targetModel,
|
||||
attachmentBlockProps,
|
||||
place
|
||||
);
|
||||
|
||||
blockIds.forEach(
|
||||
(blockId, index) =>
|
||||
void uploadAttachmentBlob(editorHost, blockId, files[index], types[index])
|
||||
);
|
||||
|
||||
return blockIds;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { effects as blockAttachmentEffects } from '@blocksuite/affine-block-attachment/effects';
|
||||
import { effects as blockBookmarkEffects } from '@blocksuite/affine-block-bookmark/effects';
|
||||
import { effects as blockEmbedEffects } from '@blocksuite/affine-block-embed/effects';
|
||||
import { effects as blockListEffects } from '@blocksuite/affine-block-list/effects';
|
||||
@@ -29,11 +30,6 @@ import { EmbedCardEditCaptionEditModal } from './_common/components/embed-card/m
|
||||
import { EmbedCardCreateModal } from './_common/components/embed-card/modal/embed-card-create-modal.js';
|
||||
import { EmbedCardEditModal } from './_common/components/embed-card/modal/embed-card-edit-modal.js';
|
||||
import { registerSpecs } from './_specs/register-specs.js';
|
||||
import { AttachmentEdgelessBlockComponent } from './attachment-block/attachment-edgeless-block.js';
|
||||
import {
|
||||
AttachmentBlockComponent,
|
||||
type AttachmentBlockService,
|
||||
} from './attachment-block/index.js';
|
||||
import { AffineCodeUnit } from './code-block/highlight/affine-code-unit.js';
|
||||
import {
|
||||
CodeBlockComponent,
|
||||
@@ -275,6 +271,7 @@ export function effects() {
|
||||
stdEffects();
|
||||
inlineEffects();
|
||||
|
||||
blockAttachmentEffects();
|
||||
blockBookmarkEffects();
|
||||
blockListEffects();
|
||||
blockParagraphEffects();
|
||||
@@ -322,13 +319,8 @@ export function effects() {
|
||||
);
|
||||
customElements.define('affine-edgeless-text', EdgelessTextBlockComponent);
|
||||
customElements.define('center-peek', CenterPeek);
|
||||
customElements.define(
|
||||
'affine-edgeless-attachment',
|
||||
AttachmentEdgelessBlockComponent
|
||||
);
|
||||
customElements.define('database-datasource-note-renderer', NoteRenderer);
|
||||
customElements.define('database-datasource-block-renderer', BlockRenderer);
|
||||
customElements.define('affine-attachment', AttachmentBlockComponent);
|
||||
customElements.define('affine-latex', LatexBlockComponent);
|
||||
customElements.define('affine-page-root', PageRootBlockComponent);
|
||||
customElements.define('edgeless-note-mask', EdgelessNoteMask);
|
||||
@@ -594,7 +586,6 @@ declare global {
|
||||
interface BlockServices {
|
||||
'affine:note': NoteBlockService;
|
||||
'affine:page': RootService;
|
||||
'affine:attachment': AttachmentBlockService;
|
||||
'affine:database': DatabaseBlockService;
|
||||
'affine:image': ImageBlockService;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ export * from './_common/test-utils/test-utils.js';
|
||||
export * from './_common/transformers/index.js';
|
||||
export { type AbstractEditor } from './_common/types.js';
|
||||
export * from './_specs/index.js';
|
||||
export * from './attachment-block/index.js';
|
||||
export * from './code-block/index.js';
|
||||
export * from './data-view-block/index.js';
|
||||
export * from './database-block/index.js';
|
||||
@@ -49,6 +48,7 @@ export {
|
||||
MiniMindmapPreview,
|
||||
} from './surface-block/mini-mindmap/index.js';
|
||||
export * from './surface-ref-block/index.js';
|
||||
export * from '@blocksuite/affine-block-attachment';
|
||||
export * from '@blocksuite/affine-block-bookmark';
|
||||
export * from '@blocksuite/affine-block-embed';
|
||||
export * from '@blocksuite/affine-block-list';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { addAttachments } from '@blocksuite/affine-block-attachment';
|
||||
import {
|
||||
CanvasElementType,
|
||||
SurfaceGroupLikeModel,
|
||||
@@ -75,7 +76,7 @@ import {
|
||||
getSortedCloneElements,
|
||||
serializeElement,
|
||||
} from '../utils/clone-utils.js';
|
||||
import { addAttachments, addImages } from '../utils/common.js';
|
||||
import { addImages } from '../utils/common.js';
|
||||
import { deleteElements } from '../utils/crud.js';
|
||||
import {
|
||||
isAttachmentBlock,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { addAttachments } from '@blocksuite/affine-block-attachment';
|
||||
import { AttachmentIcon, LinkIcon } from '@blocksuite/affine-components/icons';
|
||||
import { MAX_IMAGE_WIDTH } from '@blocksuite/affine-model';
|
||||
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
||||
@@ -14,7 +15,7 @@ import {
|
||||
} from '../../../../../_common/utils/index.js';
|
||||
import { ImageIcon } from '../../../../../image-block/styles.js';
|
||||
import type { NoteToolOption } from '../../../gfx-tool/note-tool.js';
|
||||
import { addAttachments, addImages } from '../../../utils/common.js';
|
||||
import { addImages } from '../../../utils/common.js';
|
||||
import { getTooltipWithShortcut } from '../../utils.js';
|
||||
import { EdgelessToolbarToolMixin } from '../mixins/tool.mixin.js';
|
||||
import { NOTE_MENU_ITEMS } from './note-menu-config.js';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { focusTextModel } from '@blocksuite/affine-components/rich-text';
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import {
|
||||
type AttachmentBlockProps,
|
||||
DEFAULT_NOTE_HEIGHT,
|
||||
DEFAULT_NOTE_WIDTH,
|
||||
type ImageBlockProps,
|
||||
@@ -9,10 +8,6 @@ import {
|
||||
type NoteBlockModel,
|
||||
NoteDisplayMode,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
EMBED_CARD_HEIGHT,
|
||||
EMBED_CARD_WIDTH,
|
||||
} from '@blocksuite/affine-shared/consts';
|
||||
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
||||
import type { NoteChildrenFlavour } from '@blocksuite/affine-shared/types';
|
||||
import {
|
||||
@@ -22,7 +17,6 @@ import {
|
||||
import type { BlockStdScope } from '@blocksuite/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
import {
|
||||
Bound,
|
||||
type IPoint,
|
||||
type IVec,
|
||||
Point,
|
||||
@@ -30,91 +24,10 @@ import {
|
||||
Vec,
|
||||
} from '@blocksuite/global/utils';
|
||||
|
||||
import {
|
||||
getFileType,
|
||||
uploadAttachmentBlob,
|
||||
} from '../../../attachment-block/utils.js';
|
||||
import { calcBoundByOrigin, readImageSize } from '../components/utils.js';
|
||||
import { DEFAULT_NOTE_OFFSET_X, DEFAULT_NOTE_OFFSET_Y } from './consts.js';
|
||||
import { addBlock } from './crud.js';
|
||||
|
||||
export async function addAttachments(
|
||||
std: BlockStdScope,
|
||||
files: File[],
|
||||
point?: IVec
|
||||
): Promise<string[]> {
|
||||
if (!files.length) return [];
|
||||
|
||||
const attachmentService = std.getService('affine:attachment');
|
||||
const gfx = std.get(GfxControllerIdentifier);
|
||||
|
||||
if (!attachmentService) {
|
||||
console.error('Attachment service not found');
|
||||
return [];
|
||||
}
|
||||
const maxFileSize = attachmentService.maxFileSize;
|
||||
const isSizeExceeded = files.some(file => file.size > maxFileSize);
|
||||
if (isSizeExceeded) {
|
||||
toast(
|
||||
std.host,
|
||||
`You can only upload files less than ${humanFileSize(
|
||||
maxFileSize,
|
||||
true,
|
||||
0
|
||||
)}`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
let { x, y } = gfx.viewport.center;
|
||||
if (point) [x, y] = gfx.viewport.toModelCoord(...point);
|
||||
|
||||
const CARD_STACK_GAP = 32;
|
||||
|
||||
const dropInfos: { blockId: string; file: File }[] = files.map(
|
||||
(file, index) => {
|
||||
const point = new Point(
|
||||
x + index * CARD_STACK_GAP,
|
||||
y + index * CARD_STACK_GAP
|
||||
);
|
||||
const center = Vec.toVec(point);
|
||||
const bound = Bound.fromCenter(
|
||||
center,
|
||||
EMBED_CARD_WIDTH.cubeThick,
|
||||
EMBED_CARD_HEIGHT.cubeThick
|
||||
);
|
||||
const blockId = std.doc.addBlock(
|
||||
'affine:attachment',
|
||||
{
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
style: 'cubeThick',
|
||||
xywh: bound.serialize(),
|
||||
} satisfies Partial<AttachmentBlockProps>,
|
||||
gfx.surface
|
||||
);
|
||||
|
||||
return { blockId, file };
|
||||
}
|
||||
);
|
||||
|
||||
// upload file and update the attachment model
|
||||
const uploadPromises = dropInfos.map(async ({ blockId, file }) => {
|
||||
const filetype = await getFileType(file);
|
||||
await uploadAttachmentBlob(std.host, blockId, file, filetype, true);
|
||||
return blockId;
|
||||
});
|
||||
const blockIds = await Promise.all(uploadPromises);
|
||||
|
||||
gfx.selection.set({
|
||||
elements: blockIds,
|
||||
editing: false,
|
||||
});
|
||||
|
||||
return blockIds;
|
||||
}
|
||||
|
||||
export async function addImages(
|
||||
std: BlockStdScope,
|
||||
files: File[],
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
type AttachmentBlockComponent,
|
||||
attachmentViewToggleMenu,
|
||||
} from '@blocksuite/affine-block-attachment';
|
||||
import { getEmbedCardIcons } from '@blocksuite/affine-block-embed';
|
||||
import {
|
||||
CaptionIcon,
|
||||
@@ -18,8 +22,6 @@ import { property } from 'lit/decorators.js';
|
||||
import { join } from 'lit/directives/join.js';
|
||||
|
||||
import type { EmbedCardStyle } from '../../../_common/types.js';
|
||||
import type { AttachmentBlockComponent } from '../../../attachment-block/index.js';
|
||||
import { attachmentViewToggleMenu } from '../../../attachment-block/index.js';
|
||||
import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js';
|
||||
|
||||
export class EdgelessChangeAttachmentButton extends WithDisposable(LitElement) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AttachmentBlockComponent } from '@blocksuite/affine-block-attachment';
|
||||
import type { BookmarkBlockComponent } from '@blocksuite/affine-block-bookmark';
|
||||
import type {
|
||||
EmbedFigmaBlockComponent,
|
||||
@@ -31,7 +32,6 @@ import {
|
||||
notifyDocCreated,
|
||||
promptDocTitle,
|
||||
} from '../../../../_common/utils/render-linked-doc.js';
|
||||
import type { AttachmentBlockComponent } from '../../../../attachment-block/attachment-block.js';
|
||||
import type { ImageBlockComponent } from '../../../../image-block/image-block.js';
|
||||
import { duplicate } from '../../../edgeless/utils/clipboard-utils.js';
|
||||
import { getSortedCloneElements } from '../../../edgeless/utils/clone-utils.js';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { addSiblingAttachmentBlocks } from '@blocksuite/affine-block-attachment';
|
||||
import {
|
||||
getInlineEditorByModel,
|
||||
insertContent,
|
||||
@@ -60,7 +61,6 @@ import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
import { toggleEmbedCardCreateModal } from '../../../_common/components/embed-card/modal/embed-card-create-modal.js';
|
||||
import { addSiblingAttachmentBlocks } from '../../../attachment-block/utils.js';
|
||||
import { getSurfaceBlock } from '../../../surface-ref-block/utils.js';
|
||||
import type { PageRootBlockComponent } from '../../page/page-root-block.js';
|
||||
import { formatDate, formatTime } from '../../utils/misc.js';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { addSiblingAttachmentBlocks } from '@blocksuite/affine-block-attachment';
|
||||
import {
|
||||
FigmaIcon,
|
||||
GithubIcon,
|
||||
@@ -49,7 +50,6 @@ import type { TemplateResult } from 'lit';
|
||||
|
||||
import { toggleEmbedCardCreateModal } from '../../../_common/components/embed-card/modal/embed-card-create-modal.js';
|
||||
import { textConversionConfigs } from '../../../_common/configs/text-conversion.js';
|
||||
import { addSiblingAttachmentBlocks } from '../../../attachment-block/utils.js';
|
||||
import type { DataViewBlockComponent } from '../../../data-view-block/index.js';
|
||||
import { getSurfaceBlock } from '../../../surface-ref-block/utils.js';
|
||||
import type { RootBlockComponent } from '../../types.js';
|
||||
|
||||
@@ -40,6 +40,9 @@
|
||||
{
|
||||
"path": "../affine/block-bookmark"
|
||||
},
|
||||
{
|
||||
"path": "../affine/block-attachment"
|
||||
},
|
||||
{
|
||||
"path": "../affine/data-view"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user