feat(editor): add edgeless media entry (#9949)

This commit is contained in:
Flrande
2025-02-07 06:10:11 +00:00
parent 12cc94f32a
commit 7eb1ed170c
6 changed files with 198 additions and 38 deletions

View File

@@ -264,7 +264,8 @@ export async function addSiblingAttachmentBlocks(
export async function addAttachments( export async function addAttachments(
std: BlockStdScope, std: BlockStdScope,
files: File[], files: File[],
point?: IVec point?: IVec,
transformPoint?: boolean // determines whether we should use `toModelCoord` to convert the point
): Promise<string[]> { ): Promise<string[]> {
if (!files.length) return []; if (!files.length) return [];
@@ -284,7 +285,14 @@ export async function addAttachments(
} }
let { x, y } = gfx.viewport.center; 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; const CARD_STACK_GAP = 32;

View File

@@ -428,6 +428,7 @@ export async function addImages(
options: { options: {
point?: IVec; point?: IVec;
maxWidth?: number; maxWidth?: number;
transformPoint?: boolean; // determines whether we should use `toModelCoord` to convert the point
} }
): Promise<string[]> { ): Promise<string[]> {
const imageFiles = [...files].filter(file => file.type.startsWith('image/')); const imageFiles = [...files].filter(file => file.type.startsWith('image/'));
@@ -449,9 +450,15 @@ export async function addImages(
return []; return [];
} }
const { point, maxWidth } = options; const { point, maxWidth, transformPoint = true } = options;
let { x, y } = gfx.viewport.center; 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 dropInfos: { point: Point; blockId: string }[] = [];
const IMAGE_STACK_GAP = 32; const IMAGE_STACK_GAP = 32;

View File

@@ -1,10 +1,17 @@
import { addAttachments } from '@blocksuite/affine-block-attachment';
import { insertEdgelessTextCommand } from '@blocksuite/affine-block-edgeless-text'; import { insertEdgelessTextCommand } from '@blocksuite/affine-block-edgeless-text';
import { addImages } from '@blocksuite/affine-block-image';
import { CanvasElementType } from '@blocksuite/affine-block-surface'; 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 { import {
FeatureFlagService, FeatureFlagService,
TelemetryProvider, TelemetryProvider,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import { openFileOrFiles } from '@blocksuite/affine-shared/utils';
import { assertInstanceOf, Bound } from '@blocksuite/global/utils'; import { assertInstanceOf, Bound } from '@blocksuite/global/utils';
import type { TemplateResult } from 'lit'; import type { TemplateResult } from 'lit';
import * as Y from 'yjs'; import * as Y from 'yjs';
@@ -19,7 +26,7 @@ export type ConfigStyle = Partial<Record<ConfigProperty, number | string>>;
export type ToolConfig = Record<ConfigState, ConfigStyle>; export type ToolConfig = Record<ConfigState, ConfigStyle>;
export type DraggableTool = { export type DraggableTool = {
name: 'text' | 'mindmap'; name: 'text' | 'mindmap' | 'media';
icon: TemplateResult; icon: TemplateResult;
config: ToolConfig; config: ToolConfig;
standardWidth?: number; standardWidth?: number;
@@ -27,7 +34,7 @@ export type DraggableTool = {
bound: Bound, bound: Bound,
edgelessService: EdgelessRootService, edgelessService: EdgelessRootService,
edgeless: EdgelessRootBlockComponent edgeless: EdgelessRootBlockComponent
) => string; ) => Promise<string | null>;
}; };
const unitMap = { x: 'px', y: 'px', r: 'deg', s: '', z: '', o: '' }; 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 }, next: { x: -22, y: 64, r: 0 },
}; };
export const mindmapConfig: ToolConfig = { 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 }, active: { x: 11, y: -14, r: 9, s: 1 },
hover: { x: 11, y: -14, r: 9, s: 1.16, z: 3 }, hover: { x: 11, y: -14, r: 9, s: 1.16, z: 3 },
next: { y: 64, r: 0 }, 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 = export const getMindmapRender =
(mindmapStyle: MindmapStyle): DraggableTool['render'] => (mindmapStyle: MindmapStyle): DraggableTool['render'] =>
(bound, edgelessService) => { async (bound, edgelessService) => {
const [x, y, _, h] = bound.toXYWH(); const [x, y, _, h] = bound.toXYWH();
const rootW = 145; const rootW = 145;
@@ -98,7 +111,8 @@ export const getMindmapRender =
return mindmapId; return mindmapId;
}; };
export const textRender: DraggableTool['render'] = (
export const textRender: DraggableTool['render'] = async (
bound, bound,
service, service,
edgeless edgeless
@@ -143,6 +157,41 @@ export const textRender: DraggableTool['render'] = (
return id; 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 toolStyle2StyleObj = (state: ConfigState, style: ConfigStyle = {}) => {
const styleObj = {} as Record<string, string>; const styleObj = {} as Record<string, string>;
for (const [key, value] of Object.entries(style)) { for (const [key, value] of Object.entries(style)) {

View File

@@ -574,3 +574,25 @@ export const importMindMapIcon = svg`<svg width="64" height="48" viewBox="0 0 64
<rect x="5.5" y="17.9183" width="17.3771" height="12.1639" rx="3" fill="black" fill-opacity="0.03" stroke="#929292" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="2 3"/> <rect x="5.5" y="17.9183" width="17.3771" height="12.1639" rx="3" fill="black" fill-opacity="0.03" stroke="#929292" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="2 3"/>
</svg> </svg>
`; `;
export const mindmapMenuMediaIcon = svg`<svg width="56" height="49" viewBox="0 0 56 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_215_65373)">
<path d="M4.51611 7.83333C4.51611 5.99238 6.01812 4.5 7.87095 4.5H48.129C49.9818 4.5 51.4838 5.99239 51.4838 7.83334V41.1667C51.4838 43.0076 49.9818 44.5 48.129 44.5H7.87095C6.01813 44.5 4.51611 43.0076 4.51611 41.1667V7.83333Z" fill="#F4F9FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.87096 2H48.129C51.3714 2 53.9999 4.61167 53.9999 7.83333V41.1667C53.9999 44.3883 51.3714 47 48.129 47H7.87096C4.62852 47 2 44.3883 2 41.1667V7.83333C2 4.61167 4.62852 2 7.87096 2ZM7.87096 4.5C6.01814 4.5 4.51613 5.99238 4.51613 7.83333V41.1667C4.51613 43.0076 6.01814 44.5 7.87096 44.5H48.129C49.9818 44.5 51.4838 43.0076 51.4838 41.1667V7.83333C51.4838 5.99238 49.9818 4.5 48.129 4.5H7.87096Z" fill="#3883FF"/>
<path d="M7.87095 44.5001H48.129C49.9818 44.5001 51.4838 43.0077 51.4838 41.1667V39.7435L35.8959 17.6424C33.3356 14.0123 28.3633 13.009 24.5807 15.3591L4.51611 27.8252V41.1667C4.51611 43.0077 6.01813 44.5001 7.87095 44.5001Z" fill="#3883FF"/>
<ellipse cx="5.03225" cy="5" rx="5.03225" ry="5" transform="matrix(-1 -8.76786e-08 -8.88135e-08 1 47.9972 7.41357)" fill="#3883FF"/>
</g>
<defs>
<filter id="filter0_d_215_65373" x="0" y="0" width="56" height="49" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="1"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_215_65373"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_215_65373" result="shape"/>
</filter>
</defs>
</svg>
`;

View File

@@ -22,8 +22,8 @@ import { getTooltipWithShortcut } from '../../utils.js';
import { EdgelessDraggableElementController } from '../common/draggable/draggable-element.controller.js'; import { EdgelessDraggableElementController } from '../common/draggable/draggable-element.controller.js';
import { EdgelessToolbarToolMixin } from '../mixins/tool.mixin.js'; import { EdgelessToolbarToolMixin } from '../mixins/tool.mixin.js';
import { getMindMaps, type ToolbarMindmapItem } from './assets.js'; import { getMindMaps, type ToolbarMindmapItem } from './assets.js';
import { textRender } from './basket-elements.js'; import { mediaRender, textRender } from './basket-elements.js';
import { importMindMapIcon, textIcon } from './icons.js'; import { importMindMapIcon, mindmapMenuMediaIcon, textIcon } from './icons.js';
import { MindMapPlaceholder } from './mindmap-importing-placeholder.js'; import { MindMapPlaceholder } from './mindmap-importing-placeholder.js';
type TextItem = { type TextItem = {
@@ -32,6 +32,12 @@ type TextItem = {
render: typeof textRender; render: typeof textRender;
}; };
type MediaItem = {
type: 'media';
icon: TemplateResult;
render: typeof mediaRender;
};
type ImportItem = { type ImportItem = {
type: 'import'; type: 'import';
icon: TemplateResult; icon: TemplateResult;
@@ -39,6 +45,12 @@ type ImportItem = {
const textItem: TextItem = { type: 'text', icon: textIcon, render: textRender }; const textItem: TextItem = { type: 'text', icon: textIcon, render: textRender };
const mediaItem: MediaItem = {
type: 'media',
icon: mindmapMenuMediaIcon,
render: mediaRender,
};
export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin( export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin(
SignalWatcher(LitElement) SignalWatcher(LitElement)
) { ) {
@@ -60,7 +72,8 @@ export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin(
height: 48px; height: 48px;
background: var(--affine-border-color); background: var(--affine-border-color);
} }
.text-item { .text-item,
.media-item {
width: 60px; width: 60px;
} }
.mindmap-item { .mindmap-item {
@@ -68,6 +81,7 @@ export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin(
} }
.text-item, .text-item,
.media-item,
.mindmap-item { .mindmap-item {
border-radius: 4px; border-radius: 4px;
height: 48px; height: 48px;
@@ -77,6 +91,7 @@ export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin(
justify-content: center; justify-content: center;
} }
.text-item > button, .text-item > button,
.media-item > button,
.mindmap-item > button { .mindmap-item > button {
position: absolute; position: absolute;
border-radius: inherit; border-radius: inherit;
@@ -86,11 +101,13 @@ export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin(
padding: 0; padding: 0;
} }
.text-item:hover, .text-item:hover,
.media-item:hover,
.mindmap-item[data-is-active='true'], .mindmap-item[data-is-active='true'],
.mindmap-item:hover { .mindmap-item:hover {
background: var(--affine-hover-color); background: var(--affine-hover-color);
} }
.text-item > button.next, .text-item > button.next,
.media-item > button.next,
.mindmap-item > button.next { .mindmap-item > button.next {
transition: transform 0.3s ease-in-out; transition: transform 0.3s ease-in-out;
} }
@@ -103,7 +120,7 @@ export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin(
}); });
draggableController!: EdgelessDraggableElementController< draggableController!: EdgelessDraggableElementController<
ToolbarMindmapItem | TextItem | ImportItem ToolbarMindmapItem | TextItem | ImportItem | MediaItem
>; >;
override type = 'empty' as const; override type = 'empty' as const;
@@ -215,21 +232,26 @@ export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin(
}, },
onDrop: (element, bound) => { onDrop: (element, bound) => {
if ('render' in element.data) { if ('render' in element.data) {
const id = element.data.render( element.data
bound, .render(bound, this.edgeless.service, this.edgeless)
this.edgeless.service, .then(id => {
this.edgeless if (!id) return;
); if (element.data.type === 'mindmap') {
if (element.data.type === 'mindmap') { this.onActiveStyleChange?.(element.data.style);
this.onActiveStyleChange?.(element.data.style); this.setEdgelessTool({ type: 'default' });
this.setEdgelessTool({ type: 'default' }); this.edgeless.gfx.selection.set({
this.edgeless.gfx.selection.set({ elements: [id], editing: false }); elements: [id],
} else if (element.data.type === 'text') { editing: false,
this.setEdgelessTool({ type: 'default' }); });
} } else if (
} element.data.type === 'text' ||
element.data.type === 'media'
if (element.data.type === 'import') { ) {
this.setEdgelessTool({ type: 'default' });
}
})
.catch(console.error);
} else if (element.data.type === 'import') {
this._onImportMindMap?.(bound); this._onImportMindMap?.(bound);
} }
}, },
@@ -240,10 +262,40 @@ export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin(
const { cancelled, draggingElement, dragOut } = const { cancelled, draggingElement, dragOut } =
this.draggableController?.states || {}; this.draggableController?.states || {};
const isDraggingMedia = draggingElement?.data?.type === 'media';
const isDraggingText = draggingElement?.data?.type === 'text'; const isDraggingText = draggingElement?.data?.type === 'text';
const showNextText = dragOut && !cancelled; const showNextText = dragOut && !cancelled;
return html`<edgeless-slide-menu .height=${'64px'}> return html`<edgeless-slide-menu .height=${'64px'}>
<div class="text-and-mindmap"> <div class="text-and-mindmap">
<div class="media-item">
${isDraggingMedia
? html`<button
class="next"
style="transform: translateY(${showNextText ? 0 : 64}px)"
>
${mediaItem.icon}
</button>`
: nothing}
<button
style="opacity: ${isDraggingMedia ? 0 : 1}"
@mousedown=${(e: MouseEvent) =>
this.draggableController.onMouseDown(e, {
preview: mediaItem.icon,
data: mediaItem,
})}
@touchstart=${(e: TouchEvent) =>
this.draggableController.onTouchStart(e, {
preview: mediaItem.icon,
data: mediaItem,
})}
>
${mediaItem.icon}
</button>
<affine-tooltip tip-position="top" .offset=${12}>
${getTooltipWithShortcut('Add media')}
</affine-tooltip>
</div>
<div class="thin-divider"></div>
<div class="text-item"> <div class="text-item">
${isDraggingText ${isDraggingText
? html`<button ? html`<button

View File

@@ -23,12 +23,19 @@ import { getMindMaps } from './assets.js';
import { import {
type DraggableTool, type DraggableTool,
getMindmapRender, getMindmapRender,
mediaConfig,
mediaRender,
mindmapConfig, mindmapConfig,
textConfig, textConfig,
textRender, textRender,
toolConfig2StyleObj, toolConfig2StyleObj,
} from './basket-elements.js'; } from './basket-elements.js';
import { basketIconDark, basketIconLight, textIcon } from './icons.js'; import {
basketIconDark,
basketIconLight,
mindmapMenuMediaIcon,
textIcon,
} from './icons.js';
import { importMindmap } from './utils/import-mindmap.js'; import { importMindmap } from './utils/import-mindmap.js';
export class EdgelessMindmapToolButton extends EdgelessToolbarToolMixin( export class EdgelessMindmapToolButton extends EdgelessToolbarToolMixin(
@@ -142,6 +149,13 @@ export class EdgelessMindmapToolButton extends EdgelessToolbarToolMixin(
const mindmap = const mindmap =
this.mindmaps.find(m => m.style === style) || this.mindmaps[0]; this.mindmaps.find(m => m.style === style) || this.mindmaps[0];
return [ return [
{
name: 'media',
icon: mindmapMenuMediaIcon,
config: mediaConfig,
standardWidth: 100,
render: mediaRender,
},
{ {
name: 'text', name: 'text',
icon: textIcon, icon: textIcon,
@@ -244,14 +258,22 @@ export class EdgelessMindmapToolButton extends EdgelessToolbarToolMixin(
this.readyToDrop = false; this.readyToDrop = false;
}, },
onDrop: (el, bound) => { onDrop: (el, bound) => {
const id = el.data.render(bound, this.edgeless.service, this.edgeless); el.data
this.readyToDrop = false; .render(bound, this.edgeless.service, this.edgeless)
if (el.data.name === 'mindmap') { .then(id => {
this.setEdgelessTool({ type: 'default' }); if (!id) return;
this.edgeless.gfx.selection.set({ elements: [id], editing: false }); this.readyToDrop = false;
} else if (el.data.name === 'text') { if (el.data.name === 'mindmap') {
this.setEdgelessTool({ type: 'default' }); this.setEdgelessTool({ type: 'default' });
} this.edgeless.gfx.selection.set({
elements: [id],
editing: false,
});
} else if (el.data.name === 'text') {
this.setEdgelessTool({ type: 'default' });
}
})
.catch(console.error);
}, },
}); });