💄 style: Update Modal style

pull/504/head
canisminor1990 2023-12-27 00:26:45 +08:00
parent 44a543bdd5
commit cb6fb2d60b
15 changed files with 340 additions and 324 deletions

File diff suppressed because one or more lines are too long

View File

@ -29,7 +29,7 @@ const Giscus = memo<GiscusProps>(({ open, onCancel }) => {
const { t } = useTranslation();
return (
<Modal
footer={false}
footer={null}
onCancel={onCancel}
open={open}
title={

View File

@ -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}

View File

@ -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}

View File

@ -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>

View File

@ -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}

View File

@ -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}

View File

@ -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>

View File

@ -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;

View File

@ -1,116 +1,31 @@
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}>
{children}
{withFooter ? (
<Flexbox align={'center'} className={styles.footer} gap={4}>
<Logo extra={'SD'} type={'combine'} />
<div className={styles.url}>{GITHUB_REPO_URL}</div>
</Flexbox>
) : (
<div />
)}
</Flexbox>
</div>
<div className={styles.preview}>
<div className={withBackground ? styles.background : undefined} id={'preview'}>
<Flexbox className={styles.container} gap={16}>
{children}
{withFooter ? (
<Flexbox align={'center'} className={styles.footer} gap={4}>
<Logo extra={'SD'} type={'combine'} />
<div className={styles.url}>{GITHUB_REPO_URL}</div>
</Flexbox>
) : (
<div />
)}
</Flexbox>
</div>
<Button block loading={loading} onClick={handleDownload} size={'large'} type={'primary'}>
{t('shareModal.download')}
</Button>
</>
</div>
);
});

View File

@ -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';
}

View File

@ -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;

View File

@ -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(() => {

View File

@ -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,
},
];

View File

@ -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,
};
};