From 7eb1ed170c3958017993ff1f1cf278000e6c7bb5 Mon Sep 17 00:00:00 2001 From: Flrande <1978616327@qq.com> Date: Fri, 7 Feb 2025 06:10:11 +0000 Subject: [PATCH] feat(editor): add edgeless media entry (#9949) --- .../affine/block-attachment/src/utils.ts | 12 ++- blocksuite/affine/block-image/src/utils.ts | 11 ++- .../toolbar/mindmap/basket-elements.ts | 61 +++++++++++-- .../components/toolbar/mindmap/icons.ts | 22 +++++ .../toolbar/mindmap/mindmap-menu.ts | 90 +++++++++++++++---- .../toolbar/mindmap/mindmap-tool-button.ts | 40 +++++++-- 6 files changed, 198 insertions(+), 38 deletions(-) diff --git a/blocksuite/affine/block-attachment/src/utils.ts b/blocksuite/affine/block-attachment/src/utils.ts index cba8ac8ba..defd2e420 100644 --- a/blocksuite/affine/block-attachment/src/utils.ts +++ b/blocksuite/affine/block-attachment/src/utils.ts @@ -264,7 +264,8 @@ export async function addSiblingAttachmentBlocks( export async function addAttachments( std: BlockStdScope, files: File[], - point?: IVec + point?: IVec, + transformPoint?: boolean // determines whether we should use `toModelCoord` to convert the point ): Promise { if (!files.length) return []; @@ -284,7 +285,14 @@ export async function addAttachments( } let { x, y } = gfx.viewport.center; - if (point) [x, y] = gfx.viewport.toModelCoord(...point); + if (point) { + let transform = transformPoint ?? true; + if (transform) { + [x, y] = gfx.viewport.toModelCoord(...point); + } else { + [x, y] = point; + } + } const CARD_STACK_GAP = 32; diff --git a/blocksuite/affine/block-image/src/utils.ts b/blocksuite/affine/block-image/src/utils.ts index 9164e2698..7b08ad2bc 100644 --- a/blocksuite/affine/block-image/src/utils.ts +++ b/blocksuite/affine/block-image/src/utils.ts @@ -428,6 +428,7 @@ export async function addImages( options: { point?: IVec; maxWidth?: number; + transformPoint?: boolean; // determines whether we should use `toModelCoord` to convert the point } ): Promise { const imageFiles = [...files].filter(file => file.type.startsWith('image/')); @@ -449,9 +450,15 @@ export async function addImages( return []; } - const { point, maxWidth } = options; + const { point, maxWidth, transformPoint = true } = options; let { x, y } = gfx.viewport.center; - if (point) [x, y] = gfx.viewport.toModelCoord(...point); + if (point) { + if (transformPoint) { + [x, y] = gfx.viewport.toModelCoord(...point); + } else { + [x, y] = point; + } + } const dropInfos: { point: Point; blockId: string }[] = []; const IMAGE_STACK_GAP = 32; diff --git a/blocksuite/blocks/src/root-block/edgeless/components/toolbar/mindmap/basket-elements.ts b/blocksuite/blocks/src/root-block/edgeless/components/toolbar/mindmap/basket-elements.ts index 64aec3d3f..ce2c85682 100644 --- a/blocksuite/blocks/src/root-block/edgeless/components/toolbar/mindmap/basket-elements.ts +++ b/blocksuite/blocks/src/root-block/edgeless/components/toolbar/mindmap/basket-elements.ts @@ -1,10 +1,17 @@ +import { addAttachments } from '@blocksuite/affine-block-attachment'; import { insertEdgelessTextCommand } from '@blocksuite/affine-block-edgeless-text'; +import { addImages } from '@blocksuite/affine-block-image'; import { CanvasElementType } from '@blocksuite/affine-block-surface'; -import { type MindmapStyle, TextElementModel } from '@blocksuite/affine-model'; +import { + MAX_IMAGE_WIDTH, + type MindmapStyle, + TextElementModel, +} from '@blocksuite/affine-model'; import { FeatureFlagService, TelemetryProvider, } from '@blocksuite/affine-shared/services'; +import { openFileOrFiles } from '@blocksuite/affine-shared/utils'; import { assertInstanceOf, Bound } from '@blocksuite/global/utils'; import type { TemplateResult } from 'lit'; import * as Y from 'yjs'; @@ -19,7 +26,7 @@ export type ConfigStyle = Partial>; export type ToolConfig = Record; export type DraggableTool = { - name: 'text' | 'mindmap'; + name: 'text' | 'mindmap' | 'media'; icon: TemplateResult; config: ToolConfig; standardWidth?: number; @@ -27,7 +34,7 @@ export type DraggableTool = { bound: Bound, edgelessService: EdgelessRootService, edgeless: EdgelessRootBlockComponent - ) => string; + ) => Promise; }; const unitMap = { x: 'px', y: 'px', r: 'deg', s: '', z: '', o: '' }; @@ -38,15 +45,21 @@ export const textConfig: ToolConfig = { next: { x: -22, y: 64, r: 0 }, }; export const mindmapConfig: ToolConfig = { - default: { x: 4, y: -4, s: 1, z: 1, r: -7 }, + default: { x: 4, y: -4, s: 1, z: 2, r: -7 }, active: { x: 11, y: -14, r: 9, s: 1 }, hover: { x: 11, y: -14, r: 9, s: 1.16, z: 3 }, next: { y: 64, r: 0 }, }; +export const mediaConfig: ToolConfig = { + default: { x: -20, y: -8, r: -1, s: 0.92, z: 1 }, + active: { x: -20, y: -14, r: -9, s: 1 }, + hover: { x: -20, y: -14, r: -9, s: 1.16, z: 2 }, + next: { y: 64, r: 0 }, +}; export const getMindmapRender = (mindmapStyle: MindmapStyle): DraggableTool['render'] => - (bound, edgelessService) => { + async (bound, edgelessService) => { const [x, y, _, h] = bound.toXYWH(); const rootW = 145; @@ -98,7 +111,8 @@ export const getMindmapRender = return mindmapId; }; -export const textRender: DraggableTool['render'] = ( + +export const textRender: DraggableTool['render'] = async ( bound, service, edgeless @@ -143,6 +157,41 @@ export const textRender: DraggableTool['render'] = ( return id; }; +export const mediaRender: DraggableTool['render'] = async ( + bound, + _, + edgeless +) => { + let file: File | null = null; + try { + file = await openFileOrFiles(); + } catch (e) { + console.error(e); + return null; + } + if (!file) return null; + + // image + if (file.type.startsWith('image/')) { + const [id] = await addImages(edgeless.std, [file], { + point: [bound.x, bound.y], + maxWidth: MAX_IMAGE_WIDTH, + transformPoint: false, + }); + if (id) return id; + return null; + } + + // attachment + const [id] = await addAttachments( + edgeless.std, + [file], + [bound.x, bound.y], + false + ); + return id; +}; + const toolStyle2StyleObj = (state: ConfigState, style: ConfigStyle = {}) => { const styleObj = {} as Record; for (const [key, value] of Object.entries(style)) { diff --git a/blocksuite/blocks/src/root-block/edgeless/components/toolbar/mindmap/icons.ts b/blocksuite/blocks/src/root-block/edgeless/components/toolbar/mindmap/icons.ts index 0827de502..48c4bffe3 100644 --- a/blocksuite/blocks/src/root-block/edgeless/components/toolbar/mindmap/icons.ts +++ b/blocksuite/blocks/src/root-block/edgeless/components/toolbar/mindmap/icons.ts @@ -574,3 +574,25 @@ export const importMindMapIcon = svg` `; + +export const mindmapMenuMediaIcon = svg` + + + + + + + + + + + + + + + + + + + +`; diff --git a/blocksuite/blocks/src/root-block/edgeless/components/toolbar/mindmap/mindmap-menu.ts b/blocksuite/blocks/src/root-block/edgeless/components/toolbar/mindmap/mindmap-menu.ts index 247fc3d02..674da9597 100644 --- a/blocksuite/blocks/src/root-block/edgeless/components/toolbar/mindmap/mindmap-menu.ts +++ b/blocksuite/blocks/src/root-block/edgeless/components/toolbar/mindmap/mindmap-menu.ts @@ -22,8 +22,8 @@ import { getTooltipWithShortcut } from '../../utils.js'; import { EdgelessDraggableElementController } from '../common/draggable/draggable-element.controller.js'; import { EdgelessToolbarToolMixin } from '../mixins/tool.mixin.js'; import { getMindMaps, type ToolbarMindmapItem } from './assets.js'; -import { textRender } from './basket-elements.js'; -import { importMindMapIcon, textIcon } from './icons.js'; +import { mediaRender, textRender } from './basket-elements.js'; +import { importMindMapIcon, mindmapMenuMediaIcon, textIcon } from './icons.js'; import { MindMapPlaceholder } from './mindmap-importing-placeholder.js'; type TextItem = { @@ -32,6 +32,12 @@ type TextItem = { render: typeof textRender; }; +type MediaItem = { + type: 'media'; + icon: TemplateResult; + render: typeof mediaRender; +}; + type ImportItem = { type: 'import'; icon: TemplateResult; @@ -39,6 +45,12 @@ type ImportItem = { const textItem: TextItem = { type: 'text', icon: textIcon, render: textRender }; +const mediaItem: MediaItem = { + type: 'media', + icon: mindmapMenuMediaIcon, + render: mediaRender, +}; + export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin( SignalWatcher(LitElement) ) { @@ -60,7 +72,8 @@ export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin( height: 48px; background: var(--affine-border-color); } - .text-item { + .text-item, + .media-item { width: 60px; } .mindmap-item { @@ -68,6 +81,7 @@ export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin( } .text-item, + .media-item, .mindmap-item { border-radius: 4px; height: 48px; @@ -77,6 +91,7 @@ export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin( justify-content: center; } .text-item > button, + .media-item > button, .mindmap-item > button { position: absolute; border-radius: inherit; @@ -86,11 +101,13 @@ export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin( padding: 0; } .text-item:hover, + .media-item:hover, .mindmap-item[data-is-active='true'], .mindmap-item:hover { background: var(--affine-hover-color); } .text-item > button.next, + .media-item > button.next, .mindmap-item > button.next { transition: transform 0.3s ease-in-out; } @@ -103,7 +120,7 @@ export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin( }); draggableController!: EdgelessDraggableElementController< - ToolbarMindmapItem | TextItem | ImportItem + ToolbarMindmapItem | TextItem | ImportItem | MediaItem >; override type = 'empty' as const; @@ -215,21 +232,26 @@ export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin( }, onDrop: (element, bound) => { if ('render' in element.data) { - const id = element.data.render( - bound, - this.edgeless.service, - this.edgeless - ); - if (element.data.type === 'mindmap') { - this.onActiveStyleChange?.(element.data.style); - this.setEdgelessTool({ type: 'default' }); - this.edgeless.gfx.selection.set({ elements: [id], editing: false }); - } else if (element.data.type === 'text') { - this.setEdgelessTool({ type: 'default' }); - } - } - - if (element.data.type === 'import') { + element.data + .render(bound, this.edgeless.service, this.edgeless) + .then(id => { + if (!id) return; + if (element.data.type === 'mindmap') { + this.onActiveStyleChange?.(element.data.style); + this.setEdgelessTool({ type: 'default' }); + this.edgeless.gfx.selection.set({ + elements: [id], + editing: false, + }); + } else if ( + element.data.type === 'text' || + element.data.type === 'media' + ) { + this.setEdgelessTool({ type: 'default' }); + } + }) + .catch(console.error); + } else if (element.data.type === 'import') { this._onImportMindMap?.(bound); } }, @@ -240,10 +262,40 @@ export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin( const { cancelled, draggingElement, dragOut } = this.draggableController?.states || {}; + const isDraggingMedia = draggingElement?.data?.type === 'media'; const isDraggingText = draggingElement?.data?.type === 'text'; const showNextText = dragOut && !cancelled; return html`
+
+ ${isDraggingMedia + ? html`` + : nothing} + + + ${getTooltipWithShortcut('Add media')} + +
+
${isDraggingText ? html`