💄 style: Update Modal style
parent
44a543bdd5
commit
cb6fb2d60b
File diff suppressed because one or more lines are too long
|
|
@ -29,7 +29,7 @@ const Giscus = memo<GiscusProps>(({ open, onCancel }) => {
|
|||
const { t } = useTranslation();
|
||||
return (
|
||||
<Modal
|
||||
footer={false}
|
||||
footer={null}
|
||||
onCancel={onCancel}
|
||||
open={open}
|
||||
title={
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { useTranslation } from 'react-i18next';
|
|||
import { CustomLogo } from '@/components';
|
||||
import { type WebuiSetting, selectors, useAppStore } from '@/store';
|
||||
|
||||
import Footer from './Footer';
|
||||
import {
|
||||
type NeutralColor,
|
||||
type PrimaryColor,
|
||||
|
|
@ -168,7 +167,6 @@ const SettingForm = memo(() => {
|
|||
|
||||
return (
|
||||
<Form
|
||||
footer={<Footer />}
|
||||
initialValues={setting}
|
||||
items={[theme]}
|
||||
onFinish={onFinish}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import { WebuiSetting, selectors, useAppStore } from '@/store';
|
||||
|
||||
import Footer from './Footer';
|
||||
import { SettingItemGroup } from './types';
|
||||
|
||||
const SettingForm = memo(() => {
|
||||
|
|
@ -64,7 +63,6 @@ const SettingForm = memo(() => {
|
|||
|
||||
return (
|
||||
<Form
|
||||
footer={<Footer />}
|
||||
initialValues={setting}
|
||||
items={[experimental, promptTextarea]}
|
||||
onFinish={onFinish}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,10 @@ const Footer = memo(() => {
|
|||
location.reload();
|
||||
}, []);
|
||||
|
||||
const buttonStyle = mobile ? { flex: 1 } : { margin: 0 };
|
||||
|
||||
return (
|
||||
<Flexbox gap={16} horizontal={!mobile} style={mobile ? { padding: 16, width: '100%' } : {}}>
|
||||
<Flexbox flex={1} gap={12} horizontal justify={'flex-end'}>
|
||||
<Popconfirm
|
||||
cancelText={t('cancel')}
|
||||
okText={t('confirm')}
|
||||
|
|
@ -25,11 +27,11 @@ const Footer = memo(() => {
|
|||
onConfirm={onReset}
|
||||
title={t('setting.button.reset')}
|
||||
>
|
||||
<Button block={mobile} danger style={{ borderRadius: 4 }}>
|
||||
<Button danger style={buttonStyle}>
|
||||
{t('setting.button.reset')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Button block={mobile} htmlType="submit" style={{ borderRadius: 4 }} type="primary">
|
||||
<Button htmlType="submit" style={buttonStyle} type="primary">
|
||||
{t('setting.button.submit')}
|
||||
</Button>
|
||||
</Flexbox>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import { WebuiSetting, selectors, useAppStore } from '@/store';
|
||||
|
||||
import Footer from './Footer';
|
||||
import { SettingItemGroup } from './types';
|
||||
|
||||
const SettingForm = memo(() => {
|
||||
|
|
@ -76,7 +75,6 @@ const SettingForm = memo(() => {
|
|||
|
||||
return (
|
||||
<Form
|
||||
footer={<Footer />}
|
||||
initialValues={setting}
|
||||
items={[layout, promptTextarea]}
|
||||
onFinish={onFinish}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import { type WebuiSetting, selectors, useAppStore } from '@/store';
|
||||
|
||||
import Footer from './Footer';
|
||||
import { SettingItemGroup } from './types';
|
||||
|
||||
const SettingForm = memo(() => {
|
||||
|
|
@ -135,7 +134,6 @@ const SettingForm = memo(() => {
|
|||
|
||||
return (
|
||||
<Form
|
||||
footer={<Footer />}
|
||||
initialValues={setting}
|
||||
items={[quickSettingSidebar, extraNetworkSidebar]}
|
||||
onFinish={onFinish}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { GITHUB_REPO_URL } from '@/const/url';
|
|||
|
||||
import FormAppearance from './Form/Appearance';
|
||||
import FormExperimental from './Form/Experimental';
|
||||
import Footer from './Form/Footer';
|
||||
import FormLayout from './Form/Layout';
|
||||
import FormSidebar from './Form/Sidebar';
|
||||
import Sidebar, { MobileSidebar, SettingsTabs } from './Sidebar';
|
||||
|
|
@ -35,9 +36,12 @@ const Setting = memo<SettingProps>(({ open, onCancel }) => {
|
|||
|
||||
return (
|
||||
<Modal
|
||||
footer={false}
|
||||
footer={<Footer />}
|
||||
onCancel={onCancel}
|
||||
open={open}
|
||||
styles={{
|
||||
body: mobile ? { padding: 0 } : {},
|
||||
}}
|
||||
title={
|
||||
<Flexbox align={'center'} gap={4}>
|
||||
<Flexbox align={'center'} gap={4} horizontal>
|
||||
|
|
|
|||
|
|
@ -1,127 +0,0 @@
|
|||
import { Form, ItemGroup } from '@lobehub/ui';
|
||||
import { Input, Segmented, SegmentedProps, Switch } from 'antd';
|
||||
import { Image, Share2 } from 'lucide-react';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import Preview, { ImageType, imageTypeOptions } from './Preview';
|
||||
import PreviewInner from './PreviewInner';
|
||||
|
||||
enum Tab {
|
||||
Info = 'info',
|
||||
Settings = 'settings',
|
||||
}
|
||||
|
||||
const Inner = memo<{ type: 'txt' | 'img' }>(({ type }) => {
|
||||
const [title, setTitle] = useState('');
|
||||
const [withBackground, setWithBackground] = useState(true);
|
||||
const [withFooter, setWithFooter] = useState(true);
|
||||
const [showConfig, setShowConfig] = useState(true);
|
||||
const [showNegative, setShowNegative] = useState(true);
|
||||
const [showAllImages, setShowAllImages] = useState(false);
|
||||
const [imageType, setImageType] = useState<ImageType>(ImageType.JPG);
|
||||
const [tab, setTab] = useState<Tab>(Tab.Info);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const options: SegmentedProps['options'] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: t('shareModal.tabs.info'),
|
||||
value: Tab.Info,
|
||||
},
|
||||
{
|
||||
label: t('shareModal.tabs.settings'),
|
||||
value: Tab.Settings,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const info: ItemGroup = useMemo(
|
||||
() =>
|
||||
({
|
||||
children: [
|
||||
{
|
||||
children: <Input onChange={(e) => setTitle(e.target.value)} value={title} />,
|
||||
label: t('shareModal.title'),
|
||||
},
|
||||
{
|
||||
children: <Switch checked={showAllImages} onChange={setShowAllImages} />,
|
||||
label: t('shareModal.showAllImages'),
|
||||
minWidth: undefined,
|
||||
},
|
||||
{
|
||||
children: <Switch checked={showNegative} onChange={setShowNegative} />,
|
||||
label: t('shareModal.showNegative'),
|
||||
minWidth: undefined,
|
||||
},
|
||||
{
|
||||
children: <Switch checked={showConfig} onChange={setShowConfig} />,
|
||||
label: t('shareModal.showConfig'),
|
||||
minWidth: undefined,
|
||||
},
|
||||
].filter(Boolean),
|
||||
icon: Image,
|
||||
title: t('shareModal.info'),
|
||||
}) as ItemGroup,
|
||||
[title, showAllImages, showNegative, showConfig],
|
||||
);
|
||||
|
||||
const settings: ItemGroup = useMemo(
|
||||
() =>
|
||||
({
|
||||
children: [
|
||||
{
|
||||
children: <Switch checked={withBackground} onChange={setWithBackground} />,
|
||||
label: t('shareModal.withBackground'),
|
||||
minWidth: undefined,
|
||||
},
|
||||
{
|
||||
children: <Switch checked={withFooter} onChange={setWithFooter} />,
|
||||
label: t('shareModal.withFooter'),
|
||||
minWidth: undefined,
|
||||
},
|
||||
{
|
||||
children: (
|
||||
<Segmented
|
||||
onChange={(value) => setImageType(value as ImageType)}
|
||||
options={imageTypeOptions}
|
||||
value={imageType}
|
||||
/>
|
||||
),
|
||||
label: t('shareModal.imageType'),
|
||||
minWidth: undefined,
|
||||
},
|
||||
].filter(Boolean),
|
||||
icon: Share2,
|
||||
title: t('shareModal.settings'),
|
||||
}) as ItemGroup,
|
||||
[withBackground, withFooter, imageType],
|
||||
);
|
||||
|
||||
return (
|
||||
<Flexbox gap={16}>
|
||||
<Segmented
|
||||
block
|
||||
onChange={(value) => setTab(value as Tab)}
|
||||
options={options}
|
||||
style={{ width: '100%' }}
|
||||
value={tab}
|
||||
/>
|
||||
{tab === Tab.Info && <Form items={[info]} />}
|
||||
{tab === Tab.Settings && <Form items={[settings]} />}
|
||||
<Preview imageType={imageType} withBackground={withBackground} withFooter={withFooter}>
|
||||
<PreviewInner
|
||||
showAllImages={showAllImages}
|
||||
showConfig={showConfig}
|
||||
showNegative={showNegative}
|
||||
title={title}
|
||||
type={type}
|
||||
/>
|
||||
</Preview>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default Inner;
|
||||
|
|
@ -1,97 +1,16 @@
|
|||
import { Logo } from '@lobehub/ui';
|
||||
import { Button, SegmentedProps } from 'antd';
|
||||
import { consola } from 'consola';
|
||||
import dayjs from 'dayjs';
|
||||
import { domToJpeg, domToPng, domToSvg, domToWebp } from 'modern-screenshot';
|
||||
import { PropsWithChildren, memo, useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PropsWithChildren, memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { GITHUB_REPO_URL } from '@/const/url';
|
||||
|
||||
import { useStyles } from './style';
|
||||
import { FieldType } from './type';
|
||||
|
||||
export enum ImageType {
|
||||
JPG = 'jpg',
|
||||
PNG = 'png',
|
||||
SVG = 'svg',
|
||||
WEBP = 'webp',
|
||||
}
|
||||
|
||||
export const imageTypeOptions: SegmentedProps['options'] = [
|
||||
{
|
||||
label: 'JPG',
|
||||
value: ImageType.JPG,
|
||||
},
|
||||
{
|
||||
label: 'PNG',
|
||||
value: ImageType.PNG,
|
||||
},
|
||||
{
|
||||
label: 'SVG',
|
||||
value: ImageType.SVG,
|
||||
},
|
||||
{
|
||||
label: 'WEBP',
|
||||
value: ImageType.WEBP,
|
||||
},
|
||||
];
|
||||
|
||||
interface PreviewProps extends PropsWithChildren {
|
||||
imageType: ImageType;
|
||||
withBackground: boolean;
|
||||
withFooter: boolean;
|
||||
}
|
||||
|
||||
const Preview = memo<PreviewProps>(({ imageType, withBackground, withFooter, children }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const Preview = memo<FieldType & PropsWithChildren>(({ withBackground, withFooter, children }) => {
|
||||
const { styles } = useStyles(withBackground);
|
||||
|
||||
const handleDownload = useCallback(async() => {
|
||||
setLoading(true);
|
||||
try {
|
||||
let screenshotFn: any;
|
||||
switch (imageType) {
|
||||
case ImageType.JPG: {
|
||||
screenshotFn = domToJpeg;
|
||||
break;
|
||||
}
|
||||
case ImageType.PNG: {
|
||||
screenshotFn = domToPng;
|
||||
break;
|
||||
}
|
||||
case ImageType.SVG: {
|
||||
screenshotFn = domToSvg;
|
||||
break;
|
||||
}
|
||||
case ImageType.WEBP: {
|
||||
screenshotFn = domToWebp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const dataUrl = await screenshotFn(document.querySelector('#preview') as HTMLDivElement, {
|
||||
features: {
|
||||
// 不启用移除控制符,否则会导致 safari emoji 报错
|
||||
removeControlCharacter: false,
|
||||
},
|
||||
scale: 2,
|
||||
});
|
||||
const link = document.createElement('a');
|
||||
link.download = `LobeTheme_${dayjs().format('YYYY-MM-DD')}.${imageType}`;
|
||||
link.href = dataUrl;
|
||||
link.click();
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
consola.error('🤯 Failed to download image', error);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [imageType]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.preview}>
|
||||
<div className={withBackground ? styles.background : undefined} id={'preview'}>
|
||||
<Flexbox className={styles.container} gap={16}>
|
||||
|
|
@ -107,10 +26,6 @@ const Preview = memo<PreviewProps>(({ imageType, withBackground, withFooter, chi
|
|||
</Flexbox>
|
||||
</div>
|
||||
</div>
|
||||
<Button block loading={loading} onClick={handleDownload} size={'large'} type={'primary'}>
|
||||
{t('shareModal.download')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -4,17 +4,14 @@ import { memo } from 'react';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { FieldType } from '@/features/Share/type';
|
||||
import { useGalleryObserver } from '@/hooks/useGalleryObserver';
|
||||
import { useObserver } from '@/hooks/useObserver';
|
||||
import InfoBox from '@/modules/ImageInfo/features/InfoBox';
|
||||
|
||||
import { useStyles } from './style';
|
||||
|
||||
export interface PreviewInnerProps {
|
||||
showAllImages?: boolean;
|
||||
showConfig?: boolean;
|
||||
showNegative?: boolean;
|
||||
title?: string;
|
||||
export interface PreviewInnerProps extends FieldType {
|
||||
type: 'txt' | 'img';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,149 @@
|
|||
import { Form, type FormItemProps, Modal, type ModalProps } from '@lobehub/ui';
|
||||
import { Button, Input, Segmented, SegmentedProps, Switch } from 'antd';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import Preview from './Preview';
|
||||
import PreviewInner from './PreviewInner';
|
||||
import { type FieldType, ImageType, imageTypeOptions } from './type';
|
||||
import { useScreenshot } from './useScreenshot';
|
||||
|
||||
enum Tab {
|
||||
Info = 'info',
|
||||
Settings = 'settings',
|
||||
}
|
||||
|
||||
const DEFAULT_FIELD_VALUE: FieldType = {
|
||||
imageType: ImageType.JPG,
|
||||
showAllImages: false,
|
||||
showConfig: true,
|
||||
showNegative: true,
|
||||
title: '',
|
||||
withBackground: true,
|
||||
withFooter: false,
|
||||
};
|
||||
|
||||
const ShareModal = memo<ModalProps & { type: 'txt' | 'img' }>(({ open, onCancel, type }) => {
|
||||
const [fieldValue, setFieldValue] = useState<FieldType>(DEFAULT_FIELD_VALUE);
|
||||
const [tab, setTab] = useState<Tab>(Tab.Info);
|
||||
const { t } = useTranslation();
|
||||
const { loading, onDownload } = useScreenshot(fieldValue.imageType);
|
||||
const options: SegmentedProps['options'] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: t('shareModal.tabs.info'),
|
||||
value: Tab.Info,
|
||||
},
|
||||
{
|
||||
label: t('shareModal.tabs.settings'),
|
||||
value: Tab.Settings,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const info: FormItemProps[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
children: <Input />,
|
||||
label: t('shareModal.title'),
|
||||
name: 'title',
|
||||
},
|
||||
{
|
||||
children: <Switch />,
|
||||
label: t('shareModal.showAllImages'),
|
||||
minWidth: undefined,
|
||||
name: 'showAllImages',
|
||||
valuePropName: 'checked',
|
||||
},
|
||||
{
|
||||
children: <Switch />,
|
||||
label: t('shareModal.showNegative'),
|
||||
minWidth: undefined,
|
||||
name: 'showNegative',
|
||||
valuePropName: 'checked',
|
||||
},
|
||||
{
|
||||
children: <Switch />,
|
||||
label: t('shareModal.showConfig'),
|
||||
minWidth: undefined,
|
||||
name: 'showConfig',
|
||||
valuePropName: 'checked',
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const settings: FormItemProps[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
children: <Switch />,
|
||||
label: t('shareModal.withBackground'),
|
||||
minWidth: undefined,
|
||||
name: 'withBackground',
|
||||
valuePropName: 'checked',
|
||||
},
|
||||
{
|
||||
children: <Switch />,
|
||||
label: t('shareModal.withFooter'),
|
||||
minWidth: undefined,
|
||||
name: 'withFooter',
|
||||
valuePropName: 'checked',
|
||||
},
|
||||
{
|
||||
children: <Segmented options={imageTypeOptions} />,
|
||||
label: t('shareModal.imageType'),
|
||||
minWidth: undefined,
|
||||
name: 'imageType',
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
centered={false}
|
||||
destroyOnClose
|
||||
footer={
|
||||
<Button block loading={loading} onClick={onDownload} size={'large'} type={'primary'}>
|
||||
{t('shareModal.download')}
|
||||
</Button>
|
||||
}
|
||||
onCancel={onCancel}
|
||||
open={open}
|
||||
title={t('share')}
|
||||
>
|
||||
<Flexbox gap={16}>
|
||||
<Segmented
|
||||
block
|
||||
onChange={(value) => setTab(value as Tab)}
|
||||
options={options}
|
||||
style={{ width: '100%' }}
|
||||
value={tab}
|
||||
/>
|
||||
{tab === Tab.Info && (
|
||||
<Form
|
||||
initialValues={DEFAULT_FIELD_VALUE}
|
||||
items={info}
|
||||
itemsType={'flat'}
|
||||
onValuesChange={(_, v) => setFieldValue(v)}
|
||||
/>
|
||||
)}
|
||||
{tab === Tab.Settings && (
|
||||
<Form
|
||||
initialValues={DEFAULT_FIELD_VALUE}
|
||||
items={settings}
|
||||
itemsType={'flat'}
|
||||
onValuesChange={(_, v) => setFieldValue(v)}
|
||||
/>
|
||||
)}
|
||||
<Preview {...fieldValue}>
|
||||
<PreviewInner {...fieldValue} type={type} />
|
||||
</Preview>
|
||||
</Flexbox>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ShareModal;
|
||||
|
|
@ -1,34 +1,20 @@
|
|||
import { Modal } from '@lobehub/ui';
|
||||
import { memo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useInject } from '@/hooks/useInject';
|
||||
|
||||
import Inner from './Inner';
|
||||
import ShareModal from './ShareModal';
|
||||
import { createButton } from './createButton';
|
||||
|
||||
const Share = memo<{ type: 'txt' | 'img' }>(({ type }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const buttonReference = useRef<any>(createButton(type, setOpen));
|
||||
const { t } = useTranslation();
|
||||
|
||||
useInject(buttonReference, `#image_buttons_${type}2img > .form`, {
|
||||
debug: `[layout] inject - Share ${type}`,
|
||||
inverse: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
centered={false}
|
||||
destroyOnClose
|
||||
footer={null}
|
||||
onCancel={() => setOpen(false)}
|
||||
open={open}
|
||||
title={t('share')}
|
||||
>
|
||||
<Inner type={type} />
|
||||
</Modal>
|
||||
);
|
||||
return <ShareModal onCancel={() => setOpen(false)} open={open} type={type} />;
|
||||
});
|
||||
|
||||
export default memo(() => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
import { SegmentedProps } from 'antd';
|
||||
|
||||
export enum ImageType {
|
||||
JPG = 'jpg',
|
||||
PNG = 'png',
|
||||
SVG = 'svg',
|
||||
WEBP = 'webp',
|
||||
}
|
||||
|
||||
export interface FieldType {
|
||||
imageType: ImageType;
|
||||
showAllImages: boolean;
|
||||
showConfig: boolean;
|
||||
showNegative: boolean;
|
||||
title: string;
|
||||
withBackground: boolean;
|
||||
withFooter: boolean;
|
||||
}
|
||||
|
||||
export const imageTypeOptions: SegmentedProps['options'] = [
|
||||
{
|
||||
label: 'JPG',
|
||||
value: ImageType.JPG,
|
||||
},
|
||||
{
|
||||
label: 'PNG',
|
||||
value: ImageType.PNG,
|
||||
},
|
||||
{
|
||||
label: 'SVG',
|
||||
value: ImageType.SVG,
|
||||
},
|
||||
{
|
||||
label: 'WEBP',
|
||||
value: ImageType.WEBP,
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import { consola } from 'consola';
|
||||
import dayjs from 'dayjs';
|
||||
import { domToJpeg, domToPng, domToSvg, domToWebp } from 'modern-screenshot';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { ImageType } from './type';
|
||||
|
||||
export const useScreenshot = (imageType: ImageType) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleDownload = useCallback(async() => {
|
||||
setLoading(true);
|
||||
try {
|
||||
let screenshotFn: any;
|
||||
switch (imageType) {
|
||||
case ImageType.JPG: {
|
||||
screenshotFn = domToJpeg;
|
||||
break;
|
||||
}
|
||||
case ImageType.PNG: {
|
||||
screenshotFn = domToPng;
|
||||
break;
|
||||
}
|
||||
case ImageType.SVG: {
|
||||
screenshotFn = domToSvg;
|
||||
break;
|
||||
}
|
||||
case ImageType.WEBP: {
|
||||
screenshotFn = domToWebp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const dataUrl = await screenshotFn(document.querySelector('#preview') as HTMLDivElement, {
|
||||
features: {
|
||||
// 不启用移除控制符,否则会导致 safari emoji 报错
|
||||
removeControlCharacter: false,
|
||||
},
|
||||
scale: 2,
|
||||
});
|
||||
const link = document.createElement('a');
|
||||
link.download = `LobeTheme_${dayjs().format('YYYY-MM-DD')}.${imageType}`;
|
||||
link.href = dataUrl;
|
||||
link.click();
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
consola.error('🤯 Failed to download image', error);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [imageType]);
|
||||
|
||||
return {
|
||||
loading,
|
||||
onDownload: handleDownload,
|
||||
};
|
||||
};
|
||||
Loading…
Reference in New Issue