Skip to content

Commit

Permalink
feat(core): PDF preview
Browse files Browse the repository at this point in the history
  • Loading branch information
fundon committed Nov 5, 2024
1 parent ed06e6b commit 3cd71f8
Show file tree
Hide file tree
Showing 18 changed files with 1,194 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export const workbenchViewIconNameSchema = z.enum([
'page',
'edgeless',
'journal',
'attachment',
'pdf',
]);

export const workbenchViewMetaSchema = z.object({
Expand Down
7 changes: 6 additions & 1 deletion packages/frontend/component/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
},
"peerDependencies": {
"@blocksuite/affine": "*",
"@blocksuite/icons": "2.1.67"
"@blocksuite/icons": "*"
},
"dependencies": {
"@affine/cli": "workspace:*",
Expand All @@ -24,6 +24,7 @@
"@affine/i18n": "workspace:*",
"@atlaskit/pragmatic-drag-and-drop": "^1.2.1",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
"@blocksuite/icons": "2.1.69",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@radix-ui/react-avatar": "^1.0.4",
Expand All @@ -40,11 +41,14 @@
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"@radix-ui/react-visually-hidden": "^1.1.0",
"@toeverything/pdf-viewer": "^0.1.0",
"@toeverything/theme": "^1.0.17",
"@vanilla-extract/dynamic": "^2.1.0",
"check-password-strength": "^2.0.10",
"clsx": "^2.1.0",
"dayjs": "^1.11.10",
"file-type": "^19.1.0",
"filesize": "^10.1.6",
"jotai": "^2.8.0",
"lit": "^3.1.2",
"lodash-es": "^4.17.21",
Expand All @@ -57,6 +61,7 @@
"react-paginate": "^8.2.0",
"react-router-dom": "^6.22.3",
"react-transition-state": "^2.1.1",
"react-virtuoso": "^4.7.8",
"sonner": "^1.4.41",
"swr": "^2.2.5",
"zod": "^3.22.4"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { AttachmentBlockModel } from '@blocksuite/blocks';
import { ArrowDownBigIcon, PageIcon } from '@blocksuite/icons/rc';
import clsx from 'clsx';

import { Button } from '../../ui/button';
import * as styles from './styles.css';

interface ErrorProps {
model: AttachmentBlockModel;
ext: string;
isPDF: boolean;
}

export const Error = ({ ext }: ErrorProps) => {
return (
<div className={clsx([styles.body, styles.error])}>
<PageIcon />
<h3 className={styles.errorTitle}>Unable to preview this file</h3>
<p className={styles.errorMessage}>.{ext} file type not supported.</p>
<div className={styles.errorBtns}>
<Button variant="primary" prefix={<ArrowDownBigIcon />}>
Download
</Button>
<Button>Retry</Button>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { AttachmentBlockModel } from '@blocksuite/blocks';
import { filesize } from 'filesize';
import { useMemo } from 'react';

import { Error } from './error';
import * as styles from './styles.css';
import { Titlebar } from './titlebar';
import { Viewer } from './viewer';

export type AttachmentViewerProps = {
model: AttachmentBlockModel;
};

export const AttachmentViewer = ({ model }: AttachmentViewerProps) => {
const props = useMemo(() => {
const pieces = model.name.split('.');
const ext = pieces.pop() || '';
const name = pieces.join('.');
const isPDF = ext === 'pdf';
const size = filesize(model.size);
return { model, name, ext, size, isPDF };
}, [model]);

return (
<div className={styles.viewerContainer}>
<Titlebar {...props} />
{props.isPDF ? <Viewer {...props} /> : <Error {...props} />}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';

export const viewerContainer = style({
position: 'relative',
display: 'flex',
flexDirection: 'column',
width: '100%',
height: '100%',
});

export const titlebar = style({
display: 'flex',
justifyContent: 'space-between',
height: '52px',
padding: '10px 8px',
background: cssVarV2('layer/background/primary'),
fontSize: '12px',
fontWeight: 400,
color: cssVarV2('text/secondary'),
borderTopWidth: '0.5px',
borderTopStyle: 'solid',
borderTopColor: cssVarV2('layer/insideBorder/border'),
});

export const titlebarChild = style({
selectors: {
[`${titlebar} > &`]: {
display: 'flex',
gap: '12px',
alignItems: 'center',
paddingLeft: '12px',
paddingRight: '12px',
},
'&.zoom:not(.show)': {
display: 'none',
},
},
});

export const titlebarName = style({
display: 'flex',
});

export const body = style({
display: 'flex',
flex: 1,
position: 'relative',
selectors: {
'&:before': {
position: 'absolute',
// position: 'fixed',
content: '',
top: 0,
right: 0,
bottom: 0,
left: 0,
// width: '100%',
// height: '100%',
// zIndex: -1,
},
'&:not(.gridding):before': {
backgroundColor: cssVarV2('layer/background/secondary'),
},
'&.gridding:before': {
opacity: 0.25,
backgroundSize: '20px 20px',
backgroundImage: `linear-gradient(${cssVarV2('button/grabber/default')} 1px, transparent 1px), linear-gradient(to right, ${cssVarV2('button/grabber/default')} 1px, transparent 1px)`,
},
},
});

export const virtuoso = style({
width: '100%',
});

export const error = style({
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '4px',
});

export const errorTitle = style({
fontSize: '15px',
fontWeight: 500,
lineHeight: '24px',
color: cssVarV2('text/primary'),
marginTop: '12px',
});

export const errorMessage = style({
fontSize: '12px',
fontWeight: 500,
lineHeight: '20px',
color: cssVarV2('text/tertiary'),
});

export const errorBtns = style({
display: 'flex',
flexDirection: 'column',
gap: '10px',
marginTop: '28px',
});

export const viewerPage = style({
margin: '20px auto',
maxWidth: 'calc(100% - 40px)',
background: cssVarV2('layer/white'),
boxSizing: 'border-box',
borderWidth: '1px',
borderStyle: 'solid',
borderColor: cssVarV2('layer/insideBorder/border'),
boxShadow:
'0px 4px 20px 0px var(--transparent-black-200, rgba(0, 0, 0, 0.10))',
});

export const thumbnails = style({
position: 'absolute',
boxSizing: 'border-box',
width: '120px',
padding: '12px 0',
right: '30px',
bottom: '30px',
borderRadius: '8px',
borderWidth: '1px',
borderStyle: 'solid',
borderColor: cssVarV2('layer/insideBorder/border'),
backgroundColor: cssVarV2('layer/background/primary'),
boxShadow: cssVarV2('shadow/overlayPanelShadow/2-color'),
fontSize: '12px',
fontWeight: 500,
lineHeight: '20px',
color: cssVarV2('text/secondary'),
});

export const thumbnailsPages = style({
display: 'flex',
flexDirection: 'column',
// gap: '12px',
selectors: {
'&.collapsed': {
display: 'none',
},
'&:not(.collapsed)': {
marginBottom: '8px',
},
},
});

export const thumbnailsPage = style({
margin: '0px 12px 12px',
display: 'flex',
overflow: 'clip',
// width: '100%',
borderRadius: '4px',
borderWidth: '1px',
borderStyle: 'solid',
borderColor: cssVarV2('layer/insideBorder/border'),
selectors: {
'&.selected': {
borderColor: '#29A3FA',
},
},
});

export const thumbnailsIndicator = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 12px',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import type { AttachmentBlockModel } from '@blocksuite/affine/blocks';
import {
EditIcon,
LocalDataIcon,
MoreHorizontalIcon,
ZoomDownIcon,
ZoomUpIcon,
} from '@blocksuite/icons/rc';
import clsx from 'clsx';
import { useState } from 'react';

import { IconButton } from '../../ui/button';
import { Menu, MenuItem } from '../../ui/menu';
import * as styles from './styles.css';
import { saveBufferToFile } from './utils';

const items = [
{
name: 'Rename',
icon: <EditIcon />,
action(_model: AttachmentBlockModel) {},
},
{
name: 'Download',
icon: <LocalDataIcon />,
action(model: AttachmentBlockModel) {
const { sourceId, name } = model;
if (!sourceId) return;
saveBufferToFile(sourceId, name).catch(console.error);
},
},
];

export const MenuItems = ({ model }: { model: AttachmentBlockModel }) =>
items.map(({ name, icon, action }) => (
<MenuItem key={name} onClick={() => action(model)} prefixIcon={icon}>
{name}
</MenuItem>
));

export interface TitlebarProps {
model: AttachmentBlockModel;
name: string;
ext: string;
size: string;
isPDF: boolean;
zoom?: number;
}

export const Titlebar = ({
model,
name,
ext,
size,
zoom = 100,
isPDF = false,
}: TitlebarProps) => {
const [openMenu, setOpenMenu] = useState(false);

return (
<div className={styles.titlebar}>
<div className={styles.titlebarChild}>
<div className={styles.titlebarName}>
<div>{name}</div>
<span>.{ext}</span>
</div>
<div>{size}</div>
<IconButton icon={<LocalDataIcon />}></IconButton>
<Menu
items={<MenuItems model={model} />}
rootOptions={{
open: openMenu,
onOpenChange: setOpenMenu,
}}
contentOptions={{
side: 'bottom',
align: 'center',
avoidCollisions: false,
}}
>
<IconButton icon={<MoreHorizontalIcon />}></IconButton>
</Menu>
</div>
<div
className={clsx([
styles.titlebarChild,
'zoom',
{
show: isPDF,
},
])}
>
<IconButton icon={<ZoomDownIcon />}></IconButton>
<div>{zoom}%</div>
<IconButton icon={<ZoomUpIcon />}></IconButton>
</div>
</div>
);
};
Loading

0 comments on commit 3cd71f8

Please sign in to comment.