diff --git a/blocksuite/affine/block-image/package.json b/blocksuite/affine/block-image/package.json new file mode 100644 index 0000000000..5961f76bc4 --- /dev/null +++ b/blocksuite/affine/block-image/package.json @@ -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__" + ] +} diff --git a/blocksuite/blocks/src/image-block/adapters/extension.ts b/blocksuite/affine/block-image/src/adapters/extension.ts similarity index 100% rename from blocksuite/blocks/src/image-block/adapters/extension.ts rename to blocksuite/affine/block-image/src/adapters/extension.ts diff --git a/blocksuite/blocks/src/image-block/adapters/html.ts b/blocksuite/affine/block-image/src/adapters/html.ts similarity index 100% rename from blocksuite/blocks/src/image-block/adapters/html.ts rename to blocksuite/affine/block-image/src/adapters/html.ts diff --git a/blocksuite/blocks/src/image-block/adapters/index.ts b/blocksuite/affine/block-image/src/adapters/index.ts similarity index 100% rename from blocksuite/blocks/src/image-block/adapters/index.ts rename to blocksuite/affine/block-image/src/adapters/index.ts diff --git a/blocksuite/blocks/src/image-block/adapters/markdown.ts b/blocksuite/affine/block-image/src/adapters/markdown.ts similarity index 100% rename from blocksuite/blocks/src/image-block/adapters/markdown.ts rename to blocksuite/affine/block-image/src/adapters/markdown.ts diff --git a/blocksuite/affine/block-image/src/adapters/middleware.ts b/blocksuite/affine/block-image/src/adapters/middleware.ts new file mode 100644 index 0000000000..1b72ba88b1 --- /dev/null +++ b/blocksuite/affine/block-image/src/adapters/middleware.ts @@ -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(); diff --git a/blocksuite/blocks/src/image-block/adapters/notion-html.ts b/blocksuite/affine/block-image/src/adapters/notion-html.ts similarity index 100% rename from blocksuite/blocks/src/image-block/adapters/notion-html.ts rename to blocksuite/affine/block-image/src/adapters/notion-html.ts diff --git a/blocksuite/blocks/src/image-block/commands/index.ts b/blocksuite/affine/block-image/src/commands/index.ts similarity index 100% rename from blocksuite/blocks/src/image-block/commands/index.ts rename to blocksuite/affine/block-image/src/commands/index.ts diff --git a/blocksuite/blocks/src/image-block/commands/insert-images.ts b/blocksuite/affine/block-image/src/commands/insert-images.ts similarity index 100% rename from blocksuite/blocks/src/image-block/commands/insert-images.ts rename to blocksuite/affine/block-image/src/commands/insert-images.ts diff --git a/blocksuite/blocks/src/image-block/components/image-block-fallback.ts b/blocksuite/affine/block-image/src/components/image-block-fallback.ts similarity index 97% rename from blocksuite/blocks/src/image-block/components/image-block-fallback.ts rename to blocksuite/affine/block-image/src/components/image-block-fallback.ts index f603640f55..83a59d2458 100644 --- a/blocksuite/blocks/src/image-block/components/image-block-fallback.ts +++ b/blocksuite/affine/block-image/src/components/image-block-fallback.ts @@ -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...' diff --git a/blocksuite/blocks/src/image-block/components/image-selected-rect.ts b/blocksuite/affine/block-image/src/components/image-selected-rect.ts similarity index 100% rename from blocksuite/blocks/src/image-block/components/image-selected-rect.ts rename to blocksuite/affine/block-image/src/components/image-selected-rect.ts diff --git a/blocksuite/blocks/src/image-block/components/page-image-block.ts b/blocksuite/affine/block-image/src/components/page-image-block.ts similarity index 98% rename from blocksuite/blocks/src/image-block/components/page-image-block.ts rename to blocksuite/affine/block-image/src/components/page-image-block.ts index ef5d6856fe..e9e07e684a 100644 --- a/blocksuite/blocks/src/image-block/components/page-image-block.ts +++ b/blocksuite/affine/block-image/src/components/page-image-block.ts @@ -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; diff --git a/blocksuite/blocks/src/image-block/effects.ts b/blocksuite/affine/block-image/src/effects.ts similarity index 51% rename from blocksuite/blocks/src/image-block/effects.ts rename to blocksuite/affine/block-image/src/effects.ts index a1770e2f5f..f5823d3bfe 100644 --- a/blocksuite/blocks/src/image-block/effects.ts +++ b/blocksuite/affine/block-image/src/effects.ts @@ -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; + } } } diff --git a/blocksuite/blocks/src/image-block/image-block.ts b/blocksuite/affine/block-image/src/image-block.ts similarity index 100% rename from blocksuite/blocks/src/image-block/image-block.ts rename to blocksuite/affine/block-image/src/image-block.ts diff --git a/blocksuite/blocks/src/image-block/image-edgeless-block.ts b/blocksuite/affine/block-image/src/image-edgeless-block.ts similarity index 100% rename from blocksuite/blocks/src/image-block/image-edgeless-block.ts rename to blocksuite/affine/block-image/src/image-edgeless-block.ts diff --git a/blocksuite/blocks/src/image-block/image-resize-manager.ts b/blocksuite/affine/block-image/src/image-resize-manager.ts similarity index 89% rename from blocksuite/blocks/src/image-block/image-resize-manager.ts rename to blocksuite/affine/block-image/src/image-resize-manager.ts index 3b2752e7bf..0738ee1f55 100644 --- a/blocksuite/blocks/src/image-block/image-resize-manager.ts +++ b/blocksuite/affine/block-image/src/image-resize-manager.ts @@ -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'); +} diff --git a/blocksuite/blocks/src/image-block/image-service.ts b/blocksuite/affine/block-image/src/image-service.ts similarity index 86% rename from blocksuite/blocks/src/image-block/image-service.ts rename to blocksuite/affine/block-image/src/image-service.ts index 43ea22fa3f..a3946ebbf8 100644 --- a/blocksuite/blocks/src/image-block/image-service.ts +++ b/blocksuite/affine/block-image/src/image-service.ts @@ -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, diff --git a/blocksuite/blocks/src/image-block/image-spec.ts b/blocksuite/affine/block-image/src/image-spec.ts similarity index 100% rename from blocksuite/blocks/src/image-block/image-spec.ts rename to blocksuite/affine/block-image/src/image-spec.ts diff --git a/blocksuite/affine/block-image/src/index.ts b/blocksuite/affine/block-image/src/index.ts new file mode 100644 index 0000000000..c844dcc910 --- /dev/null +++ b/blocksuite/affine/block-image/src/index.ts @@ -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'; diff --git a/blocksuite/blocks/src/image-block/styles.ts b/blocksuite/affine/block-image/src/styles.ts similarity index 99% rename from blocksuite/blocks/src/image-block/styles.ts rename to blocksuite/affine/block-image/src/styles.ts index 2717e1e057..9902692152 100644 --- a/blocksuite/blocks/src/image-block/styles.ts +++ b/blocksuite/affine/block-image/src/styles.ts @@ -28,7 +28,7 @@ export const LoadingIcon = html` `; -export const ImageIcon = html`(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 { + 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); + }); + }); + 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); +} diff --git a/blocksuite/affine/block-image/tsconfig.json b/blocksuite/affine/block-image/tsconfig.json new file mode 100644 index 0000000000..c1a5453aa5 --- /dev/null +++ b/blocksuite/affine/block-image/tsconfig.json @@ -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" + } + ] +} diff --git a/blocksuite/blocks/package.json b/blocksuite/blocks/package.json index 6892f262e5..bca3ad81da 100644 --- a/blocksuite/blocks/package.json +++ b/blocksuite/blocks/package.json @@ -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:*", diff --git a/blocksuite/blocks/src/_common/adapters/html/block-matcher.ts b/blocksuite/blocks/src/_common/adapters/html/block-matcher.ts index bc1cc623c4..93e3dfd7c9 100644 --- a/blocksuite/blocks/src/_common/adapters/html/block-matcher.ts +++ b/blocksuite/blocks/src/_common/adapters/html/block-matcher.ts @@ -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 = [ diff --git a/blocksuite/blocks/src/_common/adapters/markdown/block-matcher.ts b/blocksuite/blocks/src/_common/adapters/markdown/block-matcher.ts index 37f61094a7..9918806394 100644 --- a/blocksuite/blocks/src/_common/adapters/markdown/block-matcher.ts +++ b/blocksuite/blocks/src/_common/adapters/markdown/block-matcher.ts @@ -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'; diff --git a/blocksuite/blocks/src/_common/adapters/notion-html/block-matcher.ts b/blocksuite/blocks/src/_common/adapters/notion-html/block-matcher.ts index 1e0a81db55..8578f7d99a 100644 --- a/blocksuite/blocks/src/_common/adapters/notion-html/block-matcher.ts +++ b/blocksuite/blocks/src/_common/adapters/notion-html/block-matcher.ts @@ -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'; diff --git a/blocksuite/blocks/src/_specs/common.ts b/blocksuite/blocks/src/_specs/common.ts index adbdb3fd37..2ee23b5611 100644 --- a/blocksuite/blocks/src/_specs/common.ts +++ b/blocksuite/blocks/src/_specs/common.ts @@ -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, diff --git a/blocksuite/blocks/src/_specs/group/common.ts b/blocksuite/blocks/src/_specs/group/common.ts index 1a60cb811d..f4411b15de 100644 --- a/blocksuite/blocks/src/_specs/group/common.ts +++ b/blocksuite/blocks/src/_specs/group/common.ts @@ -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, diff --git a/blocksuite/blocks/src/effects.ts b/blocksuite/blocks/src/effects.ts index 040f7db3d8..90fdbb701d 100644 --- a/blocksuite/blocks/src/effects.ts +++ b/blocksuite/blocks/src/effects.ts @@ -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; } } } diff --git a/blocksuite/blocks/src/image-block/index.ts b/blocksuite/blocks/src/image-block/index.ts deleted file mode 100644 index b80e0df52e..0000000000 --- a/blocksuite/blocks/src/image-block/index.ts +++ /dev/null @@ -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'; diff --git a/blocksuite/blocks/src/index.ts b/blocksuite/blocks/src/index.ts index 6e3fe8b345..a524cd3081 100644 --- a/blocksuite/blocks/src/index.ts +++ b/blocksuite/blocks/src/index.ts @@ -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'; diff --git a/blocksuite/blocks/src/root-block/edgeless/clipboard/clipboard.ts b/blocksuite/blocks/src/root-block/edgeless/clipboard/clipboard.ts index b6c70e9e2e..61f63a0f62 100644 --- a/blocksuite/blocks/src/root-block/edgeless/clipboard/clipboard.ts +++ b/blocksuite/blocks/src/root-block/edgeless/clipboard/clipboard.ts @@ -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, diff --git a/blocksuite/blocks/src/root-block/edgeless/components/toolbar/note/note-menu.ts b/blocksuite/blocks/src/root-block/edgeless/components/toolbar/note/note-menu.ts index 40c8ebc45a..a7a26a2b24 100644 --- a/blocksuite/blocks/src/root-block/edgeless/components/toolbar/note/note-menu.ts +++ b/blocksuite/blocks/src/root-block/edgeless/components/toolbar/note/note-menu.ts @@ -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} "; export function generateCursorUrl( angle = 0, @@ -79,27 +74,6 @@ export function getTooltipWithShortcut( `; } -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); -} diff --git a/blocksuite/blocks/src/root-block/edgeless/utils/common.ts b/blocksuite/blocks/src/root-block/edgeless/utils/common.ts index ec7bdafdd3..bb105c4c8b 100644 --- a/blocksuite/blocks/src/root-block/edgeless/utils/common.ts +++ b/blocksuite/blocks/src/root-block/edgeless/utils/common.ts @@ -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 { - 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); - }); - }); - 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, /** diff --git a/blocksuite/blocks/src/root-block/widgets/element-toolbar/change-image-button.ts b/blocksuite/blocks/src/root-block/widgets/element-toolbar/change-image-button.ts index aa0ac93c7a..701087eb12 100644 --- a/blocksuite/blocks/src/root-block/widgets/element-toolbar/change-image-button.ts +++ b/blocksuite/blocks/src/root-block/widgets/element-toolbar/change-image-button.ts @@ -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) { diff --git a/blocksuite/blocks/src/root-block/widgets/element-toolbar/more-menu/config.ts b/blocksuite/blocks/src/root-block/widgets/element-toolbar/more-menu/config.ts index 59bc904bcd..08e2bdb0e0 100644 --- a/blocksuite/blocks/src/root-block/widgets/element-toolbar/more-menu/config.ts +++ b/blocksuite/blocks/src/root-block/widgets/element-toolbar/more-menu/config.ts @@ -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'; diff --git a/blocksuite/blocks/src/root-block/widgets/image-toolbar/context.ts b/blocksuite/blocks/src/root-block/widgets/image-toolbar/context.ts index 127cf70714..763938d5ee 100644 --- a/blocksuite/blocks/src/root-block/widgets/image-toolbar/context.ts +++ b/blocksuite/blocks/src/root-block/widgets/image-toolbar/context.ts @@ -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(); diff --git a/blocksuite/blocks/src/root-block/widgets/image-toolbar/index.ts b/blocksuite/blocks/src/root-block/widgets/image-toolbar/index.ts index 21388db713..e2abf26529 100644 --- a/blocksuite/blocks/src/root-block/widgets/image-toolbar/index.ts +++ b/blocksuite/blocks/src/root-block/widgets/image-toolbar/index.ts @@ -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'; diff --git a/blocksuite/blocks/src/root-block/widgets/image-toolbar/utils.ts b/blocksuite/blocks/src/root-block/widgets/image-toolbar/utils.ts index cd045df926..e7810c9895 100644 --- a/blocksuite/blocks/src/root-block/widgets/image-toolbar/utils.ts +++ b/blocksuite/blocks/src/root-block/widgets/image-toolbar/utils.ts @@ -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 diff --git a/blocksuite/blocks/tsconfig.json b/blocksuite/blocks/tsconfig.json index e489f45465..b609c33394 100644 --- a/blocksuite/blocks/tsconfig.json +++ b/blocksuite/blocks/tsconfig.json @@ -43,6 +43,9 @@ { "path": "../affine/block-attachment" }, + { + "path": "../affine/block-image" + }, { "path": "../affine/data-view" }, diff --git a/tools/utils/src/workspace.gen.ts b/tools/utils/src/workspace.gen.ts index 7664bf16bd..cfbf654437 100644 --- a/tools/utils/src/workspace.gen.ts +++ b/tools/utils/src/workspace.gen.ts @@ -55,6 +55,19 @@ export const PackageList = [ 'blocksuite/framework/store', ], }, + { + location: 'blocksuite/affine/block-image', + name: '@blocksuite/affine-block-image', + workspaceDependencies: [ + 'blocksuite/affine/components', + 'blocksuite/affine/model', + 'blocksuite/affine/shared', + 'blocksuite/framework/block-std', + 'blocksuite/framework/global', + 'blocksuite/framework/inline', + 'blocksuite/framework/store', + ], + }, { location: 'blocksuite/affine/block-list', name: '@blocksuite/affine-block-list', @@ -155,6 +168,7 @@ export const PackageList = [ 'blocksuite/affine/block-attachment', 'blocksuite/affine/block-bookmark', 'blocksuite/affine/block-embed', + 'blocksuite/affine/block-image', 'blocksuite/affine/block-list', 'blocksuite/affine/block-paragraph', 'blocksuite/affine/block-surface', @@ -490,6 +504,7 @@ export type PackageName = | '@blocksuite/affine-block-attachment' | '@blocksuite/affine-block-bookmark' | '@blocksuite/affine-block-embed' + | '@blocksuite/affine-block-image' | '@blocksuite/affine-block-list' | '@blocksuite/affine-block-paragraph' | '@blocksuite/affine-block-surface' diff --git a/tsconfig.project.json b/tsconfig.project.json index 8e05cf14c5..c5c59a86e2 100644 --- a/tsconfig.project.json +++ b/tsconfig.project.json @@ -10,6 +10,7 @@ { "path": "./blocksuite/affine/block-attachment" }, { "path": "./blocksuite/affine/block-bookmark" }, { "path": "./blocksuite/affine/block-embed" }, + { "path": "./blocksuite/affine/block-image" }, { "path": "./blocksuite/affine/block-list" }, { "path": "./blocksuite/affine/block-paragraph" }, { "path": "./blocksuite/affine/block-surface" }, diff --git a/yarn.lock b/yarn.lock index 7168e59d4b..aab3de00e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3294,6 +3294,29 @@ __metadata: languageName: unknown linkType: soft +"@blocksuite/affine-block-image@workspace:*, @blocksuite/affine-block-image@workspace:blocksuite/affine/block-image": + version: 0.0.0-use.local + resolution: "@blocksuite/affine-block-image@workspace:blocksuite/affine/block-image" + dependencies: + "@blocksuite/affine-components": "workspace:*" + "@blocksuite/affine-model": "workspace:*" + "@blocksuite/affine-shared": "workspace:*" + "@blocksuite/block-std": "workspace:*" + "@blocksuite/global": "workspace:*" + "@blocksuite/icons": "npm:^2.1.75" + "@blocksuite/inline": "workspace:*" + "@blocksuite/store": "workspace:*" + "@floating-ui/dom": "npm:^1.6.10" + "@lit/context": "npm:^1.1.2" + "@preact/signals-core": "npm:^1.8.0" + "@toeverything/theme": "npm:^1.1.1" + file-type: "npm:^19.5.0" + lit: "npm:^3.2.0" + minimatch: "npm:^10.0.1" + zod: "npm:^3.23.8" + languageName: unknown + linkType: soft + "@blocksuite/affine-block-list@workspace:*, @blocksuite/affine-block-list@workspace:blocksuite/affine/block-list": version: 0.0.0-use.local resolution: "@blocksuite/affine-block-list@workspace:blocksuite/affine/block-list" @@ -3485,6 +3508,7 @@ __metadata: "@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:*"