mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00:00
refactor(editor): extract image block (#9309)
This commit is contained in:
43
blocksuite/affine/block-image/package.json
Normal file
43
blocksuite/affine/block-image/package.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@blocksuite/affine-block-image",
|
||||
"description": "Image block for BlockSuite.",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test:unit": "nx vite:test --run --passWithNoTests",
|
||||
"test:unit:coverage": "nx vite:test --run --coverage",
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"keywords": [],
|
||||
"author": "toeverything",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@blocksuite/affine-components": "workspace:*",
|
||||
"@blocksuite/affine-model": "workspace:*",
|
||||
"@blocksuite/affine-shared": "workspace:*",
|
||||
"@blocksuite/block-std": "workspace:*",
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/icons": "^2.1.75",
|
||||
"@blocksuite/inline": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.1",
|
||||
"file-type": "^19.5.0",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./effects": "./src/effects.ts"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"dist",
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
]
|
||||
}
|
||||
27
blocksuite/affine/block-image/src/adapters/middleware.ts
Normal file
27
blocksuite/affine/block-image/src/adapters/middleware.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { DEFAULT_IMAGE_PROXY_ENDPOINT } from '@blocksuite/affine-shared/consts';
|
||||
import type { JobMiddleware } from '@blocksuite/store';
|
||||
|
||||
export const customImageProxyMiddleware = (
|
||||
imageProxyURL: string
|
||||
): JobMiddleware => {
|
||||
return ({ adapterConfigs }) => {
|
||||
adapterConfigs.set('imageProxy', imageProxyURL);
|
||||
};
|
||||
};
|
||||
|
||||
const imageProxyMiddlewareBuilder = () => {
|
||||
let middleware = customImageProxyMiddleware(DEFAULT_IMAGE_PROXY_ENDPOINT);
|
||||
return {
|
||||
get: () => middleware,
|
||||
set: (url: string) => {
|
||||
middleware = customImageProxyMiddleware(url);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const defaultImageProxyMiddlewarBuilder = imageProxyMiddlewareBuilder();
|
||||
|
||||
export const setImageProxyMiddlewareURL = defaultImageProxyMiddlewarBuilder.set;
|
||||
|
||||
export const defaultImageProxyMiddleware =
|
||||
defaultImageProxyMiddlewarBuilder.get();
|
||||
@@ -7,7 +7,7 @@ import { css, html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { FailedImageIcon, ImageIcon, LoadingIcon } from '../styles.js';
|
||||
import { FailedImageIcon, LoadedImageIcon, LoadingIcon } from '../styles.js';
|
||||
|
||||
export const SURFACE_IMAGE_CARD_WIDTH = 220;
|
||||
export const SURFACE_IMAGE_CARD_HEIGHT = 122;
|
||||
@@ -85,7 +85,7 @@ export class ImageBlockFallbackCard extends WithDisposable(ShadowlessElement) {
|
||||
? LoadingIcon
|
||||
: error
|
||||
? FailedImageIcon
|
||||
: ImageIcon;
|
||||
: LoadedImageIcon;
|
||||
|
||||
const titleText = loading
|
||||
? 'Loading image...'
|
||||
@@ -80,6 +80,7 @@ export class ImageBlockPageComponent extends WithDisposable(ShadowlessElement) {
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: use key map extension
|
||||
this.block.bindHotKey({
|
||||
Escape: () => {
|
||||
selection.update(selList => {
|
||||
@@ -134,6 +135,7 @@ export class ImageBlockPageComponent extends WithDisposable(ShadowlessElement) {
|
||||
|
||||
return next({ focusBlock: nextBlock });
|
||||
})
|
||||
// @ts-expect-error FIXME(command): BS-2216
|
||||
.focusBlockStart()
|
||||
.run();
|
||||
return true;
|
||||
@@ -159,6 +161,7 @@ export class ImageBlockPageComponent extends WithDisposable(ShadowlessElement) {
|
||||
|
||||
return next({ focusBlock: prevBlock });
|
||||
})
|
||||
// @ts-expect-error FIXME(command): BS-2216
|
||||
.focusBlockEnd()
|
||||
.run();
|
||||
return true;
|
||||
@@ -1,9 +1,17 @@
|
||||
import type { getImageSelectionsCommand } from '@blocksuite/affine-shared/commands';
|
||||
|
||||
import type { insertImagesCommand } from './commands/insert-images.js';
|
||||
import { ImageBlockFallbackCard } from './components/image-block-fallback.js';
|
||||
import { ImageBlockPageComponent } from './components/page-image-block.js';
|
||||
import { ImageBlockComponent } from './image-block.js';
|
||||
import { ImageEdgelessBlockComponent } from './image-edgeless-block.js';
|
||||
import type { ImageBlockService } from './image-service.js';
|
||||
|
||||
export function effects() {
|
||||
// TODO(@L-Sun): move other effects to this file
|
||||
customElements.define('affine-image', ImageBlockComponent);
|
||||
customElements.define('affine-edgeless-image', ImageEdgelessBlockComponent);
|
||||
customElements.define('affine-page-image', ImageBlockPageComponent);
|
||||
customElements.define('affine-image-fallback-card', ImageBlockFallbackCard);
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -22,5 +30,9 @@ declare global {
|
||||
*/
|
||||
insertImages: typeof insertImagesCommand;
|
||||
}
|
||||
|
||||
interface BlockServices {
|
||||
'affine:image': ImageBlockService;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,9 @@ import {
|
||||
getModelByElement,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockComponent, PointerEventState } from '@blocksuite/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../root-block/index.js';
|
||||
import { getClosestRootBlockComponent } from '../root-block/utils/query.js';
|
||||
|
||||
export class ImageResizeManager {
|
||||
private _activeComponent: BlockComponent | null = null;
|
||||
|
||||
@@ -79,9 +77,8 @@ export class ImageResizeManager {
|
||||
rootComponent.service.std.get(DocModeProvider).getEditorMode() ===
|
||||
'edgeless'
|
||||
) {
|
||||
this._zoom = (
|
||||
rootComponent as EdgelessRootBlockComponent
|
||||
).service.viewport.zoom;
|
||||
const viewport = rootComponent.std.get(GfxControllerIdentifier).viewport;
|
||||
this._zoom = viewport.zoom;
|
||||
} else {
|
||||
this._zoom = 1;
|
||||
}
|
||||
@@ -97,3 +94,7 @@ export class ImageResizeManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getClosestRootBlockComponent(el: HTMLElement): BlockComponent | null {
|
||||
return el.closest('affine-edgeless-root, affine-page-root');
|
||||
}
|
||||
@@ -8,9 +8,8 @@ import {
|
||||
import { BlockService } from '@blocksuite/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
|
||||
import { setImageProxyMiddlewareURL } from '../_common/transformers/middlewares.js';
|
||||
import { addImages } from '../root-block/edgeless/utils/common.js';
|
||||
import { addSiblingImageBlock } from './utils.js';
|
||||
import { setImageProxyMiddlewareURL } from './adapters/middleware.js';
|
||||
import { addImages, addSiblingImageBlock } from './utils.js';
|
||||
|
||||
// bytes.parse('2GB')
|
||||
const maxFileSize = 2147483648;
|
||||
@@ -29,7 +28,10 @@ export const ImageDropOption = FileDropConfigExtension({
|
||||
const imageFiles = files.filter(file => file.type.startsWith('image/'));
|
||||
if (!imageFiles.length) return false;
|
||||
|
||||
if (targetModel && !matchFlavours(targetModel, ['affine:surface'])) {
|
||||
if (
|
||||
targetModel &&
|
||||
!matchFlavours(targetModel, ['affine:surface' as BlockSuite.Flavour])
|
||||
) {
|
||||
addSiblingImageBlock(
|
||||
std.host,
|
||||
imageFiles,
|
||||
8
blocksuite/affine/block-image/src/index.ts
Normal file
8
blocksuite/affine/block-image/src/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export * from './adapters';
|
||||
export * from './image-block';
|
||||
export * from './image-edgeless-block';
|
||||
export * from './image-service';
|
||||
export * from './image-spec';
|
||||
export * from './styles';
|
||||
export { addImages, downloadImageBlob, uploadBlobForImage } from './utils';
|
||||
export { ImageSelection } from '@blocksuite/affine-shared/selection';
|
||||
@@ -28,7 +28,7 @@ export const LoadingIcon = html`<svg
|
||||
/>
|
||||
</svg>`;
|
||||
|
||||
export const ImageIcon = html`<svg
|
||||
export const LoadedImageIcon = html`<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
@@ -10,11 +10,16 @@ import {
|
||||
transformModel,
|
||||
withTempBlobData,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { EditorHost } from '@blocksuite/block-std';
|
||||
import type { BlockStdScope, EditorHost } from '@blocksuite/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import { Bound, type IVec, Point, Vec } from '@blocksuite/global/utils';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import { readImageSize } from '../root-block/edgeless/components/utils.js';
|
||||
import {
|
||||
SURFACE_IMAGE_CARD_HEIGHT,
|
||||
SURFACE_IMAGE_CARD_WIDTH,
|
||||
} from './components/image-block-fallback.js';
|
||||
import type { ImageBlockComponent } from './image-block.js';
|
||||
import type { ImageEdgelessBlockComponent } from './image-edgeless-block.js';
|
||||
|
||||
@@ -398,3 +403,139 @@ export async function turnImageIntoCardView(
|
||||
};
|
||||
transformModel(model, 'affine:attachment', attachmentProp);
|
||||
}
|
||||
|
||||
export function readImageSize(file: File) {
|
||||
return new Promise<{ width: number; height: number }>(resolve => {
|
||||
const size = { width: 0, height: 0 };
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
size.width = img.width;
|
||||
size.height = img.height;
|
||||
URL.revokeObjectURL(img.src);
|
||||
resolve(size);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(img.src);
|
||||
resolve(size);
|
||||
};
|
||||
|
||||
img.src = URL.createObjectURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
export async function addImages(
|
||||
std: BlockStdScope,
|
||||
files: File[],
|
||||
options: {
|
||||
point?: IVec;
|
||||
maxWidth?: number;
|
||||
}
|
||||
): Promise<string[]> {
|
||||
const imageFiles = [...files].filter(file => file.type.startsWith('image/'));
|
||||
if (!imageFiles.length) return [];
|
||||
|
||||
const imageService = std.getService('affine:image');
|
||||
const gfx = std.get(GfxControllerIdentifier);
|
||||
|
||||
if (!imageService) {
|
||||
console.error('Image service not found');
|
||||
return [];
|
||||
}
|
||||
|
||||
const maxFileSize = imageService.maxFileSize;
|
||||
const isSizeExceeded = imageFiles.some(file => file.size > maxFileSize);
|
||||
if (isSizeExceeded) {
|
||||
toast(
|
||||
std.host,
|
||||
`You can only upload files less than ${humanFileSize(
|
||||
maxFileSize,
|
||||
true,
|
||||
0
|
||||
)}`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const { point, maxWidth } = options;
|
||||
let { x, y } = gfx.viewport.center;
|
||||
if (point) [x, y] = gfx.viewport.toModelCoord(...point);
|
||||
|
||||
const dropInfos: { point: Point; blockId: string }[] = [];
|
||||
const IMAGE_STACK_GAP = 32;
|
||||
const isMultipleFiles = imageFiles.length > 1;
|
||||
const inTopLeft = isMultipleFiles ? true : false;
|
||||
|
||||
// create image cards without image data
|
||||
imageFiles.forEach((file, index) => {
|
||||
const point = new Point(
|
||||
x + index * IMAGE_STACK_GAP,
|
||||
y + index * IMAGE_STACK_GAP
|
||||
);
|
||||
const center = Vec.toVec(point);
|
||||
const bound = calcBoundByOrigin(center, inTopLeft);
|
||||
const blockId = std.doc.addBlock(
|
||||
'affine:image',
|
||||
{
|
||||
size: file.size,
|
||||
xywh: bound.serialize(),
|
||||
index: gfx.layer.generateIndex(),
|
||||
},
|
||||
gfx.surface
|
||||
);
|
||||
dropInfos.push({ point, blockId });
|
||||
});
|
||||
|
||||
// upload image data and update the image model
|
||||
const uploadPromises = imageFiles.map(async (file, index) => {
|
||||
const { point, blockId } = dropInfos[index];
|
||||
|
||||
const sourceId = await std.doc.blobSync.set(file);
|
||||
const imageSize = await readImageSize(file);
|
||||
|
||||
const center = Vec.toVec(point);
|
||||
// If maxWidth is provided, limit the width of the image to maxWidth
|
||||
// Otherwise, use the original width
|
||||
const width = maxWidth
|
||||
? Math.min(imageSize.width, maxWidth)
|
||||
: imageSize.width;
|
||||
const height = maxWidth
|
||||
? (imageSize.height / imageSize.width) * width
|
||||
: imageSize.height;
|
||||
const bound = calcBoundByOrigin(center, inTopLeft, width, height);
|
||||
|
||||
std.doc.withoutTransact(() => {
|
||||
gfx.updateElement(blockId, {
|
||||
sourceId,
|
||||
...imageSize,
|
||||
width,
|
||||
height,
|
||||
xywh: bound.serialize(),
|
||||
} satisfies Partial<ImageBlockProps>);
|
||||
});
|
||||
});
|
||||
await Promise.all(uploadPromises);
|
||||
|
||||
const blockIds = dropInfos.map(info => info.blockId);
|
||||
gfx.selection.set({
|
||||
elements: blockIds,
|
||||
editing: false,
|
||||
});
|
||||
if (isMultipleFiles) {
|
||||
// @ts-expect-error FIXME(command): BS-2216
|
||||
std.command.exec('autoResizeElements');
|
||||
}
|
||||
return blockIds;
|
||||
}
|
||||
|
||||
export function calcBoundByOrigin(
|
||||
point: IVec,
|
||||
inTopLeft = false,
|
||||
width = SURFACE_IMAGE_CARD_WIDTH,
|
||||
height = SURFACE_IMAGE_CARD_HEIGHT
|
||||
) {
|
||||
return inTopLeft
|
||||
? new Bound(point[0], point[1], width, height)
|
||||
: Bound.fromCenter(point, width, height);
|
||||
}
|
||||
32
blocksuite/affine/block-image/tsconfig.json
Normal file
32
blocksuite/affine/block-image/tsconfig.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src/",
|
||||
"outDir": "./dist/",
|
||||
"noEmit": false
|
||||
},
|
||||
"include": ["./src"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../framework/global"
|
||||
},
|
||||
{
|
||||
"path": "../../framework/store"
|
||||
},
|
||||
{
|
||||
"path": "../../framework/block-std"
|
||||
},
|
||||
{
|
||||
"path": "../../framework/inline"
|
||||
},
|
||||
{
|
||||
"path": "../model"
|
||||
},
|
||||
{
|
||||
"path": "../components"
|
||||
},
|
||||
{
|
||||
"path": "../shared"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
"@blocksuite/affine-block-attachment": "workspace:*",
|
||||
"@blocksuite/affine-block-bookmark": "workspace:*",
|
||||
"@blocksuite/affine-block-embed": "workspace:*",
|
||||
"@blocksuite/affine-block-image": "workspace:*",
|
||||
"@blocksuite/affine-block-list": "workspace:*",
|
||||
"@blocksuite/affine-block-paragraph": "workspace:*",
|
||||
"@blocksuite/affine-block-surface": "workspace:*",
|
||||
|
||||
@@ -7,13 +7,13 @@ import {
|
||||
EmbedSyncedDocBlockHtmlAdapterExtension,
|
||||
EmbedYoutubeBlockHtmlAdapterExtension,
|
||||
} from '@blocksuite/affine-block-embed';
|
||||
import { ImageBlockHtmlAdapterExtension } from '@blocksuite/affine-block-image';
|
||||
import { ListBlockHtmlAdapterExtension } from '@blocksuite/affine-block-list';
|
||||
import { ParagraphBlockHtmlAdapterExtension } from '@blocksuite/affine-block-paragraph';
|
||||
|
||||
import { CodeBlockHtmlAdapterExtension } from '../../../code-block/adapters/html.js';
|
||||
import { DatabaseBlockHtmlAdapterExtension } from '../../../database-block/adapters/html.js';
|
||||
import { DividerBlockHtmlAdapterExtension } from '../../../divider-block/adapters/html.js';
|
||||
import { ImageBlockHtmlAdapterExtension } from '../../../image-block/adapters/html.js';
|
||||
import { RootBlockHtmlAdapterExtension } from '../../../root-block/adapters/html.js';
|
||||
|
||||
export const defaultBlockHtmlAdapterMatchers = [
|
||||
|
||||
@@ -7,13 +7,13 @@ import {
|
||||
embedSyncedDocBlockMarkdownAdapterMatcher,
|
||||
embedYoutubeBlockMarkdownAdapterMatcher,
|
||||
} from '@blocksuite/affine-block-embed';
|
||||
import { imageBlockMarkdownAdapterMatcher } from '@blocksuite/affine-block-image';
|
||||
import { listBlockMarkdownAdapterMatcher } from '@blocksuite/affine-block-list';
|
||||
import { paragraphBlockMarkdownAdapterMatcher } from '@blocksuite/affine-block-paragraph';
|
||||
|
||||
import { codeBlockMarkdownAdapterMatcher } from '../../../code-block/adapters/markdown.js';
|
||||
import { databaseBlockMarkdownAdapterMatcher } from '../../../database-block/adapters/markdown.js';
|
||||
import { dividerBlockMarkdownAdapterMatcher } from '../../../divider-block/adapters/markdown.js';
|
||||
import { imageBlockMarkdownAdapterMatcher } from '../../../image-block/adapters/markdown.js';
|
||||
import { latexBlockMarkdownAdapterMatcher } from '../../../latex-block/adapters/markdown.js';
|
||||
import { rootBlockMarkdownAdapterMatcher } from '../../../root-block/adapters/markdown.js';
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
embedLoomBlockNotionHtmlAdapterMatcher,
|
||||
embedYoutubeBlockNotionHtmlAdapterMatcher,
|
||||
} from '@blocksuite/affine-block-embed';
|
||||
import { imageBlockNotionHtmlAdapterMatcher } from '@blocksuite/affine-block-image';
|
||||
import { listBlockNotionHtmlAdapterMatcher } from '@blocksuite/affine-block-list';
|
||||
import { paragraphBlockNotionHtmlAdapterMatcher } from '@blocksuite/affine-block-paragraph';
|
||||
import type { BlockNotionHtmlAdapterMatcher } from '@blocksuite/affine-shared/adapters';
|
||||
@@ -13,7 +14,6 @@ import type { BlockNotionHtmlAdapterMatcher } from '@blocksuite/affine-shared/ad
|
||||
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';
|
||||
import { imageBlockNotionHtmlAdapterMatcher } from '../../../image-block/adapters/notion-html.js';
|
||||
import { latexBlockNotionHtmlAdapterMatcher } from '../../../latex-block/adapters/notion-html.js';
|
||||
import { rootBlockNotionHtmlAdapterMatcher } from '../../../root-block/adapters/notion-html.js';
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AttachmentBlockSpec } from '@blocksuite/affine-block-attachment';
|
||||
import { BookmarkBlockSpec } from '@blocksuite/affine-block-bookmark';
|
||||
import { EmbedExtensions } from '@blocksuite/affine-block-embed';
|
||||
import { ImageBlockSpec } from '@blocksuite/affine-block-image';
|
||||
import { ListBlockSpec } from '@blocksuite/affine-block-list';
|
||||
import { ParagraphBlockSpec } from '@blocksuite/affine-block-paragraph';
|
||||
import { RichTextExtensions } from '@blocksuite/affine-components/rich-text';
|
||||
@@ -12,7 +13,6 @@ 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';
|
||||
import { DividerBlockSpec } from '../divider-block/divider-spec.js';
|
||||
import { ImageBlockSpec } from '../image-block/image-spec.js';
|
||||
import {
|
||||
EdgelessNoteBlockSpec,
|
||||
NoteBlockSpec,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
EmbedSyncedDocBlockSpec,
|
||||
EmbedYoutubeBlockSpec,
|
||||
} from '@blocksuite/affine-block-embed';
|
||||
import { ImageBlockSpec } from '@blocksuite/affine-block-image';
|
||||
import { ListBlockSpec } from '@blocksuite/affine-block-list';
|
||||
import { ParagraphBlockSpec } from '@blocksuite/affine-block-paragraph';
|
||||
|
||||
@@ -16,7 +17,6 @@ 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';
|
||||
import { DividerBlockSpec } from '../../divider-block/divider-spec.js';
|
||||
import { ImageBlockSpec } from '../../image-block/image-spec.js';
|
||||
import {
|
||||
EdgelessNoteBlockSpec,
|
||||
NoteBlockSpec,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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 blockImageEffects } from '@blocksuite/affine-block-image/effects';
|
||||
import { effects as blockListEffects } from '@blocksuite/affine-block-list/effects';
|
||||
import { effects as blockParagraphEffects } from '@blocksuite/affine-block-paragraph/effects';
|
||||
import { effects as blockSurfaceEffects } from '@blocksuite/affine-block-surface/effects';
|
||||
@@ -63,14 +64,6 @@ import { DividerBlockComponent } from './divider-block/index.js';
|
||||
import type { insertEdgelessTextCommand } from './edgeless-text-block/commands/insert-edgeless-text.js';
|
||||
import { EdgelessTextBlockComponent } from './edgeless-text-block/index.js';
|
||||
import { FrameBlockComponent } from './frame-block/index.js';
|
||||
import { ImageBlockFallbackCard } from './image-block/components/image-block-fallback.js';
|
||||
import { ImageBlockPageComponent } from './image-block/components/page-image-block.js';
|
||||
import { effects as blockImageEffects } from './image-block/effects.js';
|
||||
import {
|
||||
ImageBlockComponent,
|
||||
type ImageBlockService,
|
||||
ImageEdgelessBlockComponent,
|
||||
} from './image-block/index.js';
|
||||
import { effects as blockLatexEffects } from './latex-block/effects.js';
|
||||
import { LatexBlockComponent } from './latex-block/index.js';
|
||||
import type { updateBlockType } from './note-block/commands/block-type.js';
|
||||
@@ -301,11 +294,9 @@ export function effects() {
|
||||
widgetCodeToolbarEffects();
|
||||
|
||||
customElements.define('affine-database-title', DatabaseTitle);
|
||||
customElements.define('affine-image', ImageBlockComponent);
|
||||
customElements.define('data-view-header-area-icon', IconCell);
|
||||
customElements.define('affine-database-link-cell', LinkCell);
|
||||
customElements.define('affine-database-link-cell-editing', LinkCellEditing);
|
||||
customElements.define('affine-edgeless-image', ImageEdgelessBlockComponent);
|
||||
customElements.define('data-view-header-area-text', HeaderAreaTextCell);
|
||||
customElements.define(
|
||||
'data-view-header-area-text-editing',
|
||||
@@ -326,9 +317,7 @@ export function effects() {
|
||||
customElements.define('edgeless-note-mask', EdgelessNoteMask);
|
||||
customElements.define('affine-edgeless-note', EdgelessNoteBlockComponent);
|
||||
customElements.define('affine-preview-root', PreviewRootBlockComponent);
|
||||
customElements.define('affine-page-image', ImageBlockPageComponent);
|
||||
customElements.define('affine-code', CodeBlockComponent);
|
||||
customElements.define('affine-image-fallback-card', ImageBlockFallbackCard);
|
||||
customElements.define('mini-mindmap-preview', MiniMindmapPreview);
|
||||
customElements.define('affine-frame', FrameBlockComponent);
|
||||
customElements.define('mini-mindmap-surface-block', MindmapSurfaceBlock);
|
||||
@@ -587,7 +576,6 @@ declare global {
|
||||
'affine:note': NoteBlockService;
|
||||
'affine:page': RootService;
|
||||
'affine:database': DatabaseBlockService;
|
||||
'affine:image': ImageBlockService;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export * from './adapters/markdown.js';
|
||||
export * from './image-block.js';
|
||||
export * from './image-edgeless-block.js';
|
||||
export * from './image-service.js';
|
||||
export { uploadBlobForImage } from './utils.js';
|
||||
export { ImageSelection } from '@blocksuite/affine-shared/selection';
|
||||
@@ -22,7 +22,6 @@ export * from './database-block/index.js';
|
||||
export * from './divider-block/index.js';
|
||||
export * from './edgeless-text-block/index.js';
|
||||
export * from './frame-block/index.js';
|
||||
export * from './image-block/index.js';
|
||||
export * from './latex-block/index.js';
|
||||
export * from './note-block/index.js';
|
||||
export { EdgelessTemplatePanel } from './root-block/edgeless/components/toolbar/template/template-panel.js';
|
||||
@@ -51,6 +50,7 @@ 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-image';
|
||||
export * from '@blocksuite/affine-block-list';
|
||||
export * from '@blocksuite/affine-block-paragraph';
|
||||
export * from '@blocksuite/affine-block-surface';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { addAttachments } from '@blocksuite/affine-block-attachment';
|
||||
import { addImages } from '@blocksuite/affine-block-image';
|
||||
import {
|
||||
CanvasElementType,
|
||||
SurfaceGroupLikeModel,
|
||||
@@ -76,7 +77,6 @@ import {
|
||||
getSortedCloneElements,
|
||||
serializeElement,
|
||||
} from '../utils/clone-utils.js';
|
||||
import { addImages } from '../utils/common.js';
|
||||
import { deleteElements } from '../utils/crud.js';
|
||||
import {
|
||||
isAttachmentBlock,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { addAttachments } from '@blocksuite/affine-block-attachment';
|
||||
import { addImages, LoadedImageIcon } from '@blocksuite/affine-block-image';
|
||||
import { AttachmentIcon, LinkIcon } from '@blocksuite/affine-components/icons';
|
||||
import { MAX_IMAGE_WIDTH } from '@blocksuite/affine-model';
|
||||
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
||||
@@ -13,9 +14,7 @@ import {
|
||||
type NoteChildrenFlavour,
|
||||
openFileOrFiles,
|
||||
} from '../../../../../_common/utils/index.js';
|
||||
import { ImageIcon } from '../../../../../image-block/styles.js';
|
||||
import type { NoteToolOption } from '../../../gfx-tool/note-tool.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';
|
||||
@@ -118,7 +117,7 @@ export class EdgelessNoteMenu extends EdgelessToolbarToolMixin(LitElement) {
|
||||
@click=${this._addImages}
|
||||
.disabled=${this._imageLoading}
|
||||
>
|
||||
${ImageIcon}
|
||||
${LoadedImageIcon}
|
||||
</edgeless-tool-icon-button>
|
||||
|
||||
<edgeless-tool-icon-button
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { CommonUtils } from '@blocksuite/affine-block-surface';
|
||||
import type { CursorType, StandardCursor } from '@blocksuite/block-std/gfx';
|
||||
import type { IVec } from '@blocksuite/global/utils';
|
||||
import { assertExists, Bound, Vec } from '@blocksuite/global/utils';
|
||||
import { assertExists, Vec } from '@blocksuite/global/utils';
|
||||
import { css, html } from 'lit';
|
||||
|
||||
import {
|
||||
SURFACE_IMAGE_CARD_HEIGHT,
|
||||
SURFACE_IMAGE_CARD_WIDTH,
|
||||
} from '../../../image-block/components/image-block-fallback.js';
|
||||
|
||||
// "<svg width='32' height='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'><g><path fill='white' d='M13.7,18.5h3.9l0-1.5c0-1.4-1.2-2.6-2.6-2.6h-1.5v3.9l-5.8-5.8l5.8-5.8v3.9h2.3c3.1,0,5.6,2.5,5.6,5.6v2.3h3.9l-5.8,5.8L13.7,18.5z'/><path d='M20.4,19.4v-3.2c0-2.6-2.1-4.7-4.7-4.7h-3.2l0,0V9L9,12.6l3.6,3.6v-2.6l0,0H15c1.9,0,3.5,1.6,3.5,3.5v2.4l0,0h-2.6l3.6,3.6l3.6-3.6L20.4,19.4L20.4,19.4z'/></g></svg>";
|
||||
export function generateCursorUrl(
|
||||
angle = 0,
|
||||
@@ -79,27 +74,6 @@ export function getTooltipWithShortcut(
|
||||
</div>`;
|
||||
}
|
||||
|
||||
export function readImageSize(file: File) {
|
||||
return new Promise<{ width: number; height: number }>(resolve => {
|
||||
const size = { width: 0, height: 0 };
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
size.width = img.width;
|
||||
size.height = img.height;
|
||||
URL.revokeObjectURL(img.src);
|
||||
resolve(size);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(img.src);
|
||||
resolve(size);
|
||||
};
|
||||
|
||||
img.src = URL.createObjectURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
const RESIZE_CURSORS: CursorType[] = [
|
||||
'ew-resize',
|
||||
'nwse-resize',
|
||||
@@ -245,14 +219,3 @@ export function launchIntoFullscreen(element: Element) {
|
||||
element.msRequestFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
export function calcBoundByOrigin(
|
||||
point: IVec,
|
||||
inTopLeft = false,
|
||||
width = SURFACE_IMAGE_CARD_WIDTH,
|
||||
height = SURFACE_IMAGE_CARD_HEIGHT
|
||||
) {
|
||||
return inTopLeft
|
||||
? new Bound(point[0], point[1], width, height)
|
||||
: Bound.fromCenter(point, width, height);
|
||||
}
|
||||
|
||||
@@ -1,136 +1,25 @@
|
||||
import { focusTextModel } from '@blocksuite/affine-components/rich-text';
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import {
|
||||
DEFAULT_NOTE_HEIGHT,
|
||||
DEFAULT_NOTE_WIDTH,
|
||||
type ImageBlockProps,
|
||||
NOTE_MIN_HEIGHT,
|
||||
type NoteBlockModel,
|
||||
NoteDisplayMode,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
||||
import type { NoteChildrenFlavour } from '@blocksuite/affine-shared/types';
|
||||
import {
|
||||
handleNativeRangeAtPoint,
|
||||
humanFileSize,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { handleNativeRangeAtPoint } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockStdScope } from '@blocksuite/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
import {
|
||||
type IPoint,
|
||||
type IVec,
|
||||
Point,
|
||||
type Point,
|
||||
serializeXYWH,
|
||||
Vec,
|
||||
} from '@blocksuite/global/utils';
|
||||
|
||||
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 addImages(
|
||||
std: BlockStdScope,
|
||||
files: File[],
|
||||
options: {
|
||||
point?: IVec;
|
||||
maxWidth?: number;
|
||||
}
|
||||
): Promise<string[]> {
|
||||
const imageFiles = [...files].filter(file => file.type.startsWith('image/'));
|
||||
if (!imageFiles.length) return [];
|
||||
|
||||
const imageService = std.getService('affine:image');
|
||||
const gfx = std.get(GfxControllerIdentifier);
|
||||
|
||||
if (!imageService) {
|
||||
console.error('Image service not found');
|
||||
return [];
|
||||
}
|
||||
|
||||
const maxFileSize = imageService.maxFileSize;
|
||||
const isSizeExceeded = imageFiles.some(file => file.size > maxFileSize);
|
||||
if (isSizeExceeded) {
|
||||
toast(
|
||||
std.host,
|
||||
`You can only upload files less than ${humanFileSize(
|
||||
maxFileSize,
|
||||
true,
|
||||
0
|
||||
)}`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const { point, maxWidth } = options;
|
||||
let { x, y } = gfx.viewport.center;
|
||||
if (point) [x, y] = gfx.viewport.toModelCoord(...point);
|
||||
|
||||
const dropInfos: { point: Point; blockId: string }[] = [];
|
||||
const IMAGE_STACK_GAP = 32;
|
||||
const isMultipleFiles = imageFiles.length > 1;
|
||||
const inTopLeft = isMultipleFiles ? true : false;
|
||||
|
||||
// create image cards without image data
|
||||
imageFiles.forEach((file, index) => {
|
||||
const point = new Point(
|
||||
x + index * IMAGE_STACK_GAP,
|
||||
y + index * IMAGE_STACK_GAP
|
||||
);
|
||||
const center = Vec.toVec(point);
|
||||
const bound = calcBoundByOrigin(center, inTopLeft);
|
||||
const blockId = std.doc.addBlock(
|
||||
'affine:image',
|
||||
{
|
||||
size: file.size,
|
||||
xywh: bound.serialize(),
|
||||
index: gfx.layer.generateIndex(),
|
||||
},
|
||||
gfx.surface
|
||||
);
|
||||
dropInfos.push({ point, blockId });
|
||||
});
|
||||
|
||||
// upload image data and update the image model
|
||||
const uploadPromises = imageFiles.map(async (file, index) => {
|
||||
const { point, blockId } = dropInfos[index];
|
||||
|
||||
const sourceId = await std.doc.blobSync.set(file);
|
||||
const imageSize = await readImageSize(file);
|
||||
|
||||
const center = Vec.toVec(point);
|
||||
// If maxWidth is provided, limit the width of the image to maxWidth
|
||||
// Otherwise, use the original width
|
||||
const width = maxWidth
|
||||
? Math.min(imageSize.width, maxWidth)
|
||||
: imageSize.width;
|
||||
const height = maxWidth
|
||||
? (imageSize.height / imageSize.width) * width
|
||||
: imageSize.height;
|
||||
const bound = calcBoundByOrigin(center, inTopLeft, width, height);
|
||||
|
||||
std.doc.withoutTransact(() => {
|
||||
gfx.updateElement(blockId, {
|
||||
sourceId,
|
||||
...imageSize,
|
||||
width,
|
||||
height,
|
||||
xywh: bound.serialize(),
|
||||
} satisfies Partial<ImageBlockProps>);
|
||||
});
|
||||
});
|
||||
await Promise.all(uploadPromises);
|
||||
|
||||
const blockIds = dropInfos.map(info => info.blockId);
|
||||
gfx.selection.set({
|
||||
elements: blockIds,
|
||||
editing: false,
|
||||
});
|
||||
if (isMultipleFiles) {
|
||||
std.command.exec('autoResizeElements');
|
||||
}
|
||||
return blockIds;
|
||||
}
|
||||
|
||||
export function addNoteAtPoint(
|
||||
std: BlockStdScope,
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import {
|
||||
downloadImageBlob,
|
||||
type ImageBlockComponent,
|
||||
} from '@blocksuite/affine-block-image';
|
||||
import { CaptionIcon, DownloadIcon } from '@blocksuite/affine-components/icons';
|
||||
import type { ImageBlockModel } from '@blocksuite/affine-model';
|
||||
import { WithDisposable } from '@blocksuite/global/utils';
|
||||
import { html, LitElement, nothing } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import type { ImageBlockComponent } from '../../../image-block/image-block.js';
|
||||
import { downloadImageBlob } from '../../../image-block/utils.js';
|
||||
import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js';
|
||||
|
||||
export class EdgelessChangeImageButton extends WithDisposable(LitElement) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
EmbedLoomBlockComponent,
|
||||
EmbedYoutubeBlockComponent,
|
||||
} from '@blocksuite/affine-block-embed';
|
||||
import type { ImageBlockComponent } from '@blocksuite/affine-block-image';
|
||||
import { isPeekable, peek } from '@blocksuite/affine-components/peek';
|
||||
import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar';
|
||||
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
||||
@@ -32,7 +33,6 @@ import {
|
||||
notifyDocCreated,
|
||||
promptDocTitle,
|
||||
} from '../../../../_common/utils/render-linked-doc.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';
|
||||
import { moveConnectors } from '../../../edgeless/utils/connector.js';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { ImageBlockComponent } from '@blocksuite/affine-block-image';
|
||||
import { MenuContext } from '@blocksuite/affine-components/toolbar';
|
||||
|
||||
import type { ImageBlockComponent } from '../../../image-block/image-block.js';
|
||||
|
||||
export class ImageToolbarContext extends MenuContext {
|
||||
override close = () => {
|
||||
this.abortController.abort();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ImageBlockComponent } from '@blocksuite/affine-block-image';
|
||||
import { HoverController } from '@blocksuite/affine-components/hover';
|
||||
import type {
|
||||
AdvancedMenuItem,
|
||||
@@ -13,7 +14,6 @@ import { WidgetComponent } from '@blocksuite/block-std';
|
||||
import { limitShift, shift } from '@floating-ui/dom';
|
||||
import { html } from 'lit';
|
||||
|
||||
import type { ImageBlockComponent } from '../../../image-block/image-block.js';
|
||||
import { MORE_GROUPS, PRIMARY_GROUPS } from './config.js';
|
||||
import { ImageToolbarContext } from './context.js';
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { ImageBlockComponent } from '@blocksuite/affine-block-image';
|
||||
import {
|
||||
getBlockProps,
|
||||
isInsidePageEditor,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
|
||||
import type { ImageBlockComponent } from '../../../image-block/image-block.js';
|
||||
|
||||
export function duplicate(
|
||||
block: ImageBlockComponent,
|
||||
abortController?: AbortController
|
||||
|
||||
@@ -43,6 +43,9 @@
|
||||
{
|
||||
"path": "../affine/block-attachment"
|
||||
},
|
||||
{
|
||||
"path": "../affine/block-image"
|
||||
},
|
||||
{
|
||||
"path": "../affine/data-view"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user