Skip to content

Commit 45663c3

Browse files
jrr997jrr997
and
jrr997
authored
💄 style: support screenshot to clipboard when sharing (#6275)
Co-authored-by: jrr997 <[email protected]>
1 parent e1c1260 commit 45663c3

File tree

3 files changed

+109
-51
lines changed

3 files changed

+109
-51
lines changed

src/features/ShareModal/ShareImage/index.tsx

+27-11
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { Form, type FormItemProps } from '@lobehub/ui';
1+
import { Form, type FormItemProps, Icon } from '@lobehub/ui';
22
import { Button, Segmented, Switch } from 'antd';
3+
import { CopyIcon } from 'lucide-react';
34
import { memo, useState } from 'react';
45
import { useTranslation } from 'react-i18next';
56
import { Flexbox } from 'react-layout-kit';
67

78
import { FORM_STYLE } from '@/const/layoutTokens';
9+
import { useImgToClipboard } from '@/hooks/useImgToClipboard';
810
import { useIsMobile } from '@/hooks/useIsMobile';
911
import { ImageType, imageTypeOptions, useScreenshot } from '@/hooks/useScreenshot';
1012
import { useSessionStore } from '@/store/session';
@@ -32,7 +34,9 @@ const ShareImage = memo<{ mobile?: boolean }>(({ mobile }) => {
3234
title: currentAgentTitle,
3335
width: mobile ? 720 : undefined,
3436
});
35-
37+
const { loading: copyLoading, onCopy } = useImgToClipboard({
38+
width: mobile ? 720 : undefined,
39+
});
3640
const settings: FormItemProps[] = [
3741
{
3842
children: <Switch />,
@@ -66,15 +70,27 @@ const ShareImage = memo<{ mobile?: boolean }>(({ mobile }) => {
6670
const isMobile = useIsMobile();
6771

6872
const button = (
69-
<Button
70-
block
71-
loading={loading}
72-
onClick={onDownload}
73-
size={isMobile ? undefined : 'large'}
74-
type={'primary'}
75-
>
76-
{t('shareModal.download')}
77-
</Button>
73+
<>
74+
<Button
75+
block
76+
icon={<Icon icon={CopyIcon} />}
77+
loading={copyLoading}
78+
onClick={() => onCopy()}
79+
size={isMobile ? undefined : 'large'}
80+
type={'primary'}
81+
>
82+
{t('copy', { ns: 'common' })}
83+
</Button>
84+
<Button
85+
block
86+
loading={loading}
87+
onClick={onDownload}
88+
size={isMobile ? undefined : 'large'}
89+
variant={'filled'}
90+
>
91+
{t('shareModal.download')}
92+
</Button>
93+
</>
7894
);
7995

8096
return (

src/hooks/useImgToClipboard.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { App } from 'antd';
2+
import { t } from 'i18next';
3+
import { useState } from 'react';
4+
5+
import { ImageType, getImageUrl } from './useScreenshot';
6+
7+
export const useImgToClipboard = ({ id = '#preview', width }: { id?: string; width?: number }) => {
8+
const [loading, setLoading] = useState(false);
9+
const { message } = App.useApp();
10+
11+
const handleCopy = async () => {
12+
setLoading(true);
13+
try {
14+
const dataUrl = await getImageUrl({ id, imageType: ImageType.PNG, width });
15+
const blob = await fetch(dataUrl).then((res) => res.blob());
16+
navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
17+
setLoading(false);
18+
message.success(t('copySuccess', { defaultValue: 'Copy Success', ns: 'common' }));
19+
} catch (error) {
20+
console.error('Failed to copy image', error);
21+
setLoading(false);
22+
}
23+
};
24+
25+
return {
26+
loading,
27+
onCopy: handleCopy,
28+
};
29+
};

src/hooks/useScreenshot.ts

+53-40
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,58 @@ export const imageTypeOptions: SegmentedProps['options'] = [
3131
},
3232
];
3333

34+
export const getImageUrl = async ({
35+
imageType,
36+
id = '#preview',
37+
width,
38+
}: {
39+
id?: string;
40+
imageType: ImageType;
41+
width?: number;
42+
}) => {
43+
let screenshotFn: any;
44+
switch (imageType) {
45+
case ImageType.JPG: {
46+
screenshotFn = domToJpeg;
47+
break;
48+
}
49+
case ImageType.PNG: {
50+
screenshotFn = domToPng;
51+
break;
52+
}
53+
case ImageType.SVG: {
54+
screenshotFn = domToSvg;
55+
break;
56+
}
57+
case ImageType.WEBP: {
58+
screenshotFn = domToWebp;
59+
break;
60+
}
61+
}
62+
63+
const dom: HTMLDivElement = document.querySelector(id) as HTMLDivElement;
64+
let copy: HTMLDivElement = dom;
65+
66+
if (width) {
67+
copy = dom.cloneNode(true) as HTMLDivElement;
68+
copy.style.width = `${width}px`;
69+
document.body.append(copy);
70+
}
71+
72+
const dataUrl = await screenshotFn(width ? copy : dom, {
73+
features: {
74+
// 不启用移除控制符,否则会导致 safari emoji 报错
75+
removeControlCharacter: false,
76+
},
77+
scale: 2,
78+
width,
79+
});
80+
81+
if (width && copy) copy?.remove();
82+
83+
return dataUrl;
84+
};
85+
3486
export const useScreenshot = ({
3587
imageType,
3688
title = 'share',
@@ -47,46 +99,7 @@ export const useScreenshot = ({
4799
const handleDownload = useCallback(async () => {
48100
setLoading(true);
49101
try {
50-
let screenshotFn: any;
51-
switch (imageType) {
52-
case ImageType.JPG: {
53-
screenshotFn = domToJpeg;
54-
break;
55-
}
56-
case ImageType.PNG: {
57-
screenshotFn = domToPng;
58-
break;
59-
}
60-
case ImageType.SVG: {
61-
screenshotFn = domToSvg;
62-
break;
63-
}
64-
case ImageType.WEBP: {
65-
screenshotFn = domToWebp;
66-
break;
67-
}
68-
}
69-
70-
const dom: HTMLDivElement = document.querySelector(id) as HTMLDivElement;
71-
let copy: HTMLDivElement = dom;
72-
73-
if (width) {
74-
copy = dom.cloneNode(true) as HTMLDivElement;
75-
copy.style.width = `${width}px`;
76-
document.body.append(copy);
77-
}
78-
79-
const dataUrl = await screenshotFn(width ? copy : dom, {
80-
features: {
81-
// 不启用移除控制符,否则会导致 safari emoji 报错
82-
removeControlCharacter: false,
83-
},
84-
scale: 2,
85-
width,
86-
});
87-
88-
if (width && copy) copy?.remove();
89-
102+
const dataUrl = await getImageUrl({ id, imageType, width });
90103
const link = document.createElement('a');
91104
link.download = `${BRANDING_NAME}_${title}_${dayjs().format('YYYY-MM-DD')}.${imageType}`;
92105
link.href = dataUrl;

0 commit comments

Comments
 (0)