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
---
.../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}
/>
-
+
-
+
}
type="plain"
- className={buttonStyle}
onClick={() => {
assertExists(blockId);
downloadHandler(blockId).catch(err => {
@@ -420,7 +406,6 @@ const ImagePreviewModalImpl = (
data-testid="copy-to-clipboard-button"
icon={}
type="plain"
- className={buttonStyle}
onClick={() => {
if (!imageRef.current) {
return;
@@ -455,14 +440,14 @@ const ImagePreviewModalImpl = (
}}
/>
-
+
}
type="plain"
- className={buttonStyle}
- onClick={() => blockId && deleteHandler(blockId)}
+ disabled={blocks.length === 0}
+ onClick={() => deleteHandler(cursor)}
/>
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);
{