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

@@ -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<Record<ConfigProperty, number | string>>;
export type ToolConfig = Record<ConfigState, ConfigStyle>;
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<string | null>;
};
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<string, string>;
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"/>
</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 { 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`<edgeless-slide-menu .height=${'64px'}>
<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">
${isDraggingText
? html`<button

View File

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