From 729631ea723a75f1db48f34122656dd23537b851 Mon Sep 17 00:00:00 2001 From: fundon Date: Fri, 14 Jun 2024 02:02:46 +0000 Subject: [PATCH] refactor(core): image preview toolbar UI (#7207) Closes: [AFF-1257](https://linear.app/affine-design/issue/AFF-1257/image-preview-toolbar) * refactor logic * update UI style Screenshot 2024-06-13 at 07 21 52 Screenshot 2024-06-13 at 07 21 33 --- .../src/components/image-preview/index.css.ts | 50 ++-- .../components/image-preview/index.jotai.ts | 2 + .../src/components/image-preview/index.tsx | 227 ++++++++---------- tests/affine-local/e2e/image-preview.spec.ts | 22 +- 4 files changed, 155 insertions(+), 146 deletions(-) diff --git a/packages/frontend/core/src/components/image-preview/index.css.ts b/packages/frontend/core/src/components/image-preview/index.css.ts index 52a2e7d1c6..172ab57529 100644 --- a/packages/frontend/core/src/components/image-preview/index.css.ts +++ b/packages/frontend/core/src/components/image-preview/index.css.ts @@ -107,32 +107,44 @@ export const imagePreviewModalCaptionStyle = style({ }, }); export const imagePreviewActionBarStyle = style({ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - backgroundColor: cssVar('white'), - borderRadius: '8px', - boxShadow: '2px 2px 4px rgba(0, 0, 0, 0.3)', + height: '36px', maxWidth: 'max-content', - minHeight: '44px', - maxHeight: '44px', + padding: '0 6px', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + gap: '8px', + borderRadius: '4px', + border: `0.5px solid ${cssVar('borderColor')}`, + backgroundColor: cssVar('white'), + boxShadow: '0px 6px 16px 0px rgba(0, 0, 0, 0.14)', + boxSizing: 'content-box', + color: cssVar('iconColor'), + userSelect: 'none', }); -export const groupStyle = style({ - padding: '10px 0', - boxSizing: 'border-box', +export const cursorStyle = style({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: '24px', + minWidth: '34px', + padding: '1px 2px', + fontSize: '14px', +}); +export const dividerStyle = style({ + width: '0.5px', + height: '100%', + background: cssVar('borderColor'), display: 'flex', alignItems: 'center', justifyContent: 'center', - borderLeft: '1px solid #E3E2E4', -}); -export const buttonStyle = style({ - margin: '10px 6px', }); export const scaleIndicatorButtonStyle = style({ - minHeight: '100%', - maxWidth: 'max-content', - fontSize: '12px', - padding: '5px 5px', + height: '24px', + padding: '1px 2px', + minWidth: '50px', + fontSize: '14px', + color: `${cssVar('iconColor')} !important`, ':hover': { backgroundColor: cssVar('hoverColor'), }, diff --git a/packages/frontend/core/src/components/image-preview/index.jotai.ts b/packages/frontend/core/src/components/image-preview/index.jotai.ts index 0322bc78b6..cb69bdac19 100644 --- a/packages/frontend/core/src/components/image-preview/index.jotai.ts +++ b/packages/frontend/core/src/components/image-preview/index.jotai.ts @@ -1,6 +1,8 @@ +import type { ImageBlockModel } from '@blocksuite/blocks'; import { atom } from 'jotai'; export const previewBlockIdAtom = atom(null); +export const previewblocksAtom = atom([]); export const hasAnimationPlayedAtom = atom(true); previewBlockIdAtom.onMount = set => { diff --git a/packages/frontend/core/src/components/image-preview/index.tsx b/packages/frontend/core/src/components/image-preview/index.tsx index ca73c88153..21e35e5443 100644 --- a/packages/frontend/core/src/components/image-preview/index.tsx +++ b/packages/frontend/core/src/components/image-preview/index.tsx @@ -13,7 +13,7 @@ import { PlusIcon, ViewBarIcon, } from '@blocksuite/icons'; -import type { DocCollection } from '@blocksuite/store'; +import type { BlockModel, DocCollection } from '@blocksuite/store'; import clsx from 'clsx'; import { useErrorBoundary } from 'foxact/use-error-boundary'; import { useAtom } from 'jotai'; @@ -26,9 +26,9 @@ import useSWR from 'swr'; import { useZoomControls } from './hooks/use-zoom'; import { - buttonStyle, captionStyle, - groupStyle, + cursorStyle, + dividerStyle, imageBottomContainerStyle, imagePreviewActionBarStyle, imagePreviewBackgroundStyle, @@ -41,7 +41,15 @@ import { scaleIndicatorButtonStyle, unloaded, } from './index.css'; -import { hasAnimationPlayedAtom, previewBlockIdAtom } from './index.jotai'; +import { + hasAnimationPlayedAtom, + previewBlockIdAtom, + previewblocksAtom, +} from './index.jotai'; + +const filterImageBlock = (block: BlockModel): block is ImageBlockModel => { + return block.flavour === 'affine:image'; +}; export type ImagePreviewModalProps = { docCollection: DocCollection; @@ -54,7 +62,9 @@ const ImagePreviewModalImpl = ( onClose: () => void; } ): ReactElement | null => { + const [blocks, setBlocks] = useAtom(previewblocksAtom); const [blockId, setBlockId] = useAtom(previewBlockIdAtom); + const [cursor, setCursor] = useState(0); const zoomRef = useRef(null); const imageRef = useRef(null); const { @@ -88,98 +98,72 @@ const ImagePreviewModalImpl = ( return; }, [isOpen, props, setIsOpen]); - const nextImageHandler = useCallback( - (blockId: string | null) => { - assertExists(blockId); - const workspace = props.docCollection; + const goto = useCallback( + (index: number) => { if (!hasPlayedAnimation) { setHasPlayedAnimation(true); } - const page = workspace.getDoc(props.pageId); - assertExists(page); - const block = page.getBlockById(blockId); - assertExists(block); - const nextBlock = page - .getNexts(block) - .find( - (block): block is ImageBlockModel => block.flavour === 'affine:image' - ); - if (nextBlock) { - setBlockId(nextBlock.id); - } - }, - [props.pageId, props.docCollection, setBlockId, hasPlayedAnimation] - ); - const previousImageHandler = useCallback( - (blockId: string | null) => { - assertExists(blockId); const workspace = props.docCollection; const page = workspace.getDoc(props.pageId); assertExists(page); - const block = page.getBlockById(blockId); - assertExists(block); - const prevBlock = page - .getPrevs(block) - .findLast( - (block): block is ImageBlockModel => block.flavour === 'affine:image' - ); - if (prevBlock) { - setBlockId(prevBlock.id); - } + + const block = blocks[index]; + + if (!block) return; + + setCursor(index); + setBlockId(block.id); + resetZoom(); }, - [props.pageId, props.docCollection, setBlockId, resetZoom] + [ + props.pageId, + props.docCollection, + blocks, + setBlockId, + hasPlayedAnimation, + resetZoom, + ] ); const deleteHandler = useCallback( - (blockId: string) => { + (index: number) => { const { pageId, docCollection: workspace, onClose } = props; const page = workspace.getDoc(pageId); assertExists(page); - const block = page.getBlockById(blockId); - assertExists(block); - if ( - page - .getPrevs(block) - .some( - (block): block is ImageBlockModel => - block.flavour === 'affine:image' - ) - ) { - const prevBlock = page - .getPrevs(block) - .findLast( - (block): block is ImageBlockModel => - block.flavour === 'affine:image' - ); - if (prevBlock) { - setBlockId(prevBlock.id); - } - } else if ( - page - .getNexts(block) - .some( - (block): block is ImageBlockModel => - block.flavour === 'affine:image' - ) - ) { - const nextBlock = page - .getNexts(block) - .find( - (block): block is ImageBlockModel => - block.flavour === 'affine:image' - ); - if (nextBlock) { - setBlockId(nextBlock.id); - } - } else { - onClose(); - } + + let block = blocks[index]; + + if (!block) return; + + blocks.splice(index, 1); + setBlocks([...blocks]); + page.deleteBlock(block); + + // next + block = blocks[index]; + + // prev + if (!block) { + index -= 1; + block = blocks[index]; + + if (!block) { + onClose(); + return; + } + + setCursor(index); + } + + setBlockId(block.id); + + resetZoom(); }, - [props, setBlockId] + [props, blocks, setBlockId, setBlocks, setCursor, resetZoom] ); const downloadHandler = useCallback( @@ -188,7 +172,7 @@ const ImagePreviewModalImpl = ( const page = workspace.getDoc(props.pageId); assertExists(page); if (typeof blockId === 'string') { - const block = page.getBlockById(blockId) as ImageBlockModel; + const block = page.getBlockById(blockId); assertExists(block); const store = block.page.blobSync; const url = store?.get(block.sourceId as string); @@ -238,27 +222,39 @@ const ImagePreviewModalImpl = ( }, [props.pageId, props.docCollection] ); + const [caption, setCaption] = useState(() => { const page = props.docCollection.getDoc(props.pageId); assertExists(page); - const block = page.getBlockById(props.blockId) as ImageBlockModel; + const block = page.getBlockById(props.blockId); assertExists(block); return block?.caption; }); + useEffect(() => { const page = props.docCollection.getDoc(props.pageId); assertExists(page); - const block = page.getBlockById(props.blockId) as ImageBlockModel; + + const block = page.getBlockById(props.blockId); assertExists(block); + + const prevs = page.getPrevs(block).filter(filterImageBlock); + const nexts = page.getNexts(block).filter(filterImageBlock); + + const blocks = [...prevs, block, ...nexts]; + setBlocks(blocks); + setCursor(blocks.length ? prevs.length : 0); + setCaption(block?.caption); - }, [props.blockId, props.pageId, props.docCollection]); + }, [props.blockId, props.pageId, props.docCollection, setBlocks]); + const { data, error } = useSWR( ['workspace', 'image', props.pageId, props.blockId], { fetcher: ([_, __, pageId, blockId]) => { const page = props.docCollection.getDoc(pageId); assertExists(page); - const block = page.getBlockById(blockId) as ImageBlockModel; + const block = page.getBlockById(blockId); assertExists(block); return props.docCollection.blobSync.get(block?.sourceId as string); }, @@ -335,39 +331,33 @@ const ImagePreviewModalImpl = (

) : null}
-
- - } - type="plain" - className={buttonStyle} - onClick={() => { - assertExists(blockId); - previousImageHandler(blockId); - }} - /> - - - } - className={buttonStyle} - type="plain" - onClick={() => { - assertExists(blockId); - nextImageHandler(blockId); - }} - /> - + + } + type="plain" + disabled={cursor < 1} + onClick={() => goto(cursor - 1)} + /> + +
+ {`${blocks.length ? cursor + 1 : 0}/${blocks.length}`}
-
- + + } + type="plain" + disabled={cursor + 1 === blocks.length} + onClick={() => goto(cursor + 1)} + /> + +
+ } type="plain" - className={buttonStyle} onClick={() => resetZoom()} /> @@ -375,16 +365,14 @@ const ImagePreviewModalImpl = ( } - className={buttonStyle} type="plain" onClick={zoomOut} />
- +
diff --git a/tests/affine-local/e2e/image-preview.spec.ts b/tests/affine-local/e2e/image-preview.spec.ts index 66f2b68e3f..467f6aff20 100644 --- a/tests/affine-local/e2e/image-preview.spec.ts +++ b/tests/affine-local/e2e/image-preview.spec.ts @@ -582,6 +582,9 @@ test('tooltips for all buttons should be visible when hovering', async ({ await title.click(); await page.keyboard.press('Enter'); await importImage(page, 'http://localhost:8081/large-image.png'); + await page.locator('affine-page-image').first().click(); + await page.keyboard.press('Enter'); + await importImage(page, 'http://localhost:8081/large-image.png'); await page.locator('affine-page-image').first().dblclick(); await page.waitForTimeout(500); blobId = (await page @@ -593,13 +596,8 @@ test('tooltips for all buttons should be visible when hovering', async ({ } const locator = page.getByTestId('image-preview-modal'); await page.waitForTimeout(500); - await locator.getByTestId('previous-image-button').hover(); + await locator.getByTestId('previous-image-button').isDisabled(); await page.waitForTimeout(1000); - { - const element = page.getByRole('tooltip'); - const previousImageTooltip = await element.getByText('Previous').count(); - expect(previousImageTooltip).toBe(1); - } await locator.getByTestId('next-image-button').hover(); await page.waitForTimeout(1000); @@ -609,6 +607,18 @@ test('tooltips for all buttons should be visible when hovering', async ({ expect(nextImageTooltip).toBe(1); } + await locator.getByTestId('next-image-button').click(); + await locator.getByTestId('next-image-button').isDisabled(); + await page.waitForTimeout(1000); + + await locator.getByTestId('previous-image-button').hover(); + await page.waitForTimeout(1000); + { + const element = page.getByRole('tooltip'); + const previousImageTooltip = await element.getByText('Previous').count(); + expect(previousImageTooltip).toBe(1); + } + await locator.getByTestId('fit-to-screen-button').hover(); await page.waitForTimeout(1000); {