image-beautifier 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/lib/image-beautifier.es.js +2852 -0
  2. package/lib/style.css +9 -0
  3. package/package.json +11 -3
  4. package/.eslintrc.cjs +0 -25
  5. package/favicon.svg +0 -20
  6. package/index.html +0 -13
  7. package/jsconfig.json +0 -12
  8. package/postcss.config.mjs +0 -6
  9. package/preview.png +0 -0
  10. package/public/vite.svg +0 -1
  11. package/src/App.jsx +0 -29
  12. package/src/assets/blur.svg +0 -8
  13. package/src/assets/color.svg +0 -1
  14. package/src/assets/demo.png +0 -0
  15. package/src/assets/pencil.png +0 -0
  16. package/src/assets/rotate.png +0 -0
  17. package/src/components/ColorPicker.jsx +0 -27
  18. package/src/components/Icon.jsx +0 -81
  19. package/src/components/editor/Editor.jsx +0 -19
  20. package/src/components/editor/HotKeys.jsx +0 -48
  21. package/src/components/editor/View.jsx +0 -167
  22. package/src/components/editor/Zoom.jsx +0 -54
  23. package/src/components/editor/layers/FrameBox.jsx +0 -51
  24. package/src/components/editor/layers/Screenshot.jsx +0 -89
  25. package/src/components/editor/layers/ShapeLine.jsx +0 -97
  26. package/src/components/editor/layers/Watermark.jsx +0 -41
  27. package/src/components/header/EmojiSelect.jsx +0 -35
  28. package/src/components/header/Header.jsx +0 -134
  29. package/src/components/header/Logo.jsx +0 -29
  30. package/src/components/header/MediaLogo.jsx +0 -10
  31. package/src/components/header/WidthDropdown.jsx +0 -74
  32. package/src/components/init/Init.jsx +0 -77
  33. package/src/components/sideBar/BackgroundSelect.jsx +0 -49
  34. package/src/components/sideBar/CropperImage.jsx +0 -73
  35. package/src/components/sideBar/CustomSize.jsx +0 -60
  36. package/src/components/sideBar/DownloadBar.jsx +0 -179
  37. package/src/components/sideBar/DrawerBar.jsx +0 -64
  38. package/src/components/sideBar/Position.jsx +0 -45
  39. package/src/components/sideBar/SideBar.jsx +0 -131
  40. package/src/components/sideBar/SizeBar.jsx +0 -114
  41. package/src/components/sideBar/Watermark.jsx +0 -59
  42. package/src/hooks/useKeyboardShortcuts.js +0 -28
  43. package/src/hooks/usePaste.js +0 -21
  44. package/src/index.js +0 -1
  45. package/src/main.jsx +0 -9
  46. package/src/stores/editor.js +0 -106
  47. package/src/stores/index.js +0 -7
  48. package/src/stores/option.js +0 -96
  49. package/src/style/main.css +0 -132
  50. package/src/utils/UndoRedoManager.js +0 -83
  51. package/src/utils/backgroundConfig.js +0 -387
  52. package/src/utils/captureScreen.js +0 -28
  53. package/src/utils/sizeConfig.js +0 -163
  54. package/src/utils/utils.js +0 -154
  55. package/tailwind.config.mjs +0 -15
  56. package/vite.config.js +0 -21
@@ -1,49 +0,0 @@
1
- import { Radio } from 'antd';
2
- import backgroundConfig from '@utils/backgroundConfig';
3
- import { cn } from '@utils/utils';
4
-
5
- const isImg = ['cosmic', 'desktop'];
6
-
7
- export const BackgroundSelect = ({ type, options, onChange, value }) => {
8
- let lists = [];
9
- if (options && options.length) {
10
- lists = options;
11
- } else {
12
- const arr = [];
13
- Object.keys(backgroundConfig).map((key) => {
14
- if (key.includes(type)) {
15
- arr.push({
16
- key,
17
- value: backgroundConfig[key],
18
- });
19
- }
20
- });
21
- lists = arr;
22
- }
23
- return (
24
- <Radio.Group
25
- onChange={(e) => onChange(e.target.value)}
26
- value={value}
27
- rootClassName={cn(
28
- 'grid [&_span]:ps-0',
29
- isImg.includes(type) ? 'grid-cols-5 gap-y-1.5' : 'grid-cols-7 gap-y-3'
30
- )}
31
- >
32
- {lists.map((item, index) => (
33
- <Radio
34
- key={index}
35
- className='[&_.ant-radio]:hidden [&_span]:p-0 mr-0'
36
- value={item.key}
37
- >
38
- {isImg.includes(type) ? (
39
- <div className={cn('w-12 h-8 rounded-md overflow-hidden')}>
40
- <img src={`${item.value.class}&w=48`} className='w-full h-full object-cover object-center' />
41
- </div>
42
- ) : (
43
- <div className={cn('w-8 h-8 rounded-full overflow-hidden', item.value.class)}></div>
44
- )}
45
- </Radio>
46
- ))}
47
- </Radio.Group>
48
- );
49
- };
@@ -1,73 +0,0 @@
1
- import { useState, useRef } from 'react';
2
- import { observer } from 'mobx-react-lite';
3
- import Icon from '@components/Icon';
4
- import { Button, Tooltip, Modal } from 'antd';
5
- import stores from '@stores';
6
- import Cropper from "react-cropper";
7
- import "cropperjs/dist/cropper.css";
8
-
9
- export default observer(() => {
10
- const cropperRef = useRef(null);
11
- const [isModalOpen, setIsModalOpen] = useState(false);
12
- const handleCrop = () => {
13
- setIsModalOpen(true);
14
- };
15
- const handleOk = () => {
16
- if (typeof cropperRef.current?.cropper !== "undefined") {
17
- const canvas = cropperRef.current?.cropper.getCroppedCanvas();
18
- if (canvas) {
19
- const { width, height } = canvas;
20
- const imgUrl = canvas.toDataURL();
21
- stores.editor.setImg(Object.assign({}, stores.editor.img, {
22
- src: imgUrl,
23
- width,
24
- height,
25
- }));
26
- if (stores.option.size.type === 'auto') {
27
- const margin = Math.round(width * 0.2);
28
- stores.option.setFrameSize(width + margin, height + margin);
29
- }
30
- }
31
- }
32
- setIsModalOpen(false);
33
- };
34
- const handleCancel = () => {
35
- setIsModalOpen(false);
36
- };
37
- return (
38
- <>
39
- <Tooltip title='Crop Image'>
40
- <Button
41
- type='text'
42
- shape='circle'
43
- icon={<Icon.Crop size={18} />}
44
- onClick={handleCrop}
45
- ></Button>
46
- </Tooltip>
47
- <Modal
48
- title='Cropper'
49
- open={isModalOpen}
50
- onOk={handleOk}
51
- onCancel={handleCancel}
52
- destroyOnClose={true}
53
- >
54
- <Cropper
55
- ref={cropperRef}
56
- style={{ height: 400, width: "100%" }}
57
- zoomTo={0.5}
58
- initialAspectRatio={stores.editor.img.width / stores.editor.img.height}
59
- src={stores.editor.img.src}
60
- dragMode="move"
61
- viewMode={1}
62
- minCropBoxHeight={10}
63
- minCropBoxWidth={10}
64
- background={false}
65
- responsive={true}
66
- autoCropArea={1}
67
- checkOrientation={false}
68
- guides={true}
69
- />
70
- </Modal>
71
- </>
72
- );
73
- });
@@ -1,60 +0,0 @@
1
- import { useState, useEffect } from 'react';
2
- import Icon from '@components/Icon';
3
- import { InputNumber, Button, Tooltip } from 'antd';
4
-
5
- export default ({ frameWidth, frameHeight, type, onSet }) => {
6
- const [width, setWidth] = useState('');
7
- const [height, setHeight] = useState('');
8
- const setAuto = () => {
9
- onSet({ type: 'auto', title: 'Auto' });
10
- };
11
- const setCustom = () => {
12
- onSet({ type: 'custom', title: 'Custom', width, height });
13
- };
14
- useEffect(() => {
15
- if (type === 'custom') {
16
- setWidth(frameWidth);
17
- setHeight(frameHeight);
18
- } else {
19
- setWidth('');
20
- setHeight('');
21
- }
22
- }, [type]);
23
- return (
24
- <div className='flex gap-2 items-center py-2 font-normal'>
25
- <InputNumber
26
- min={1}
27
- value={width}
28
- onChange={setWidth}
29
- placeholder={frameWidth}
30
- prefix={<span className='opacity-60 mx-1'>W</span>}
31
- className='flex-1'
32
- />
33
- <span className='text-xs opacity-50'>x</span>
34
- <InputNumber
35
- min={1}
36
- value={height}
37
- onChange={setHeight}
38
- placeholder={frameHeight}
39
- prefix={<span className='opacity-60 mx-1'>H</span>}
40
- className='flex-1'
41
- />
42
- <Button
43
- type='primary'
44
- shape='circle'
45
- icon={<Icon.Check size={18} />}
46
- disabled={!width || !height}
47
- onClick={setCustom}
48
- ></Button>
49
- <Tooltip title="Auto size">
50
- <Button
51
- type='primary'
52
- shape='circle'
53
- icon={<Icon.Maximize size={18} />}
54
- disabled={type === 'auto'}
55
- onClick={setAuto}
56
- ></Button>
57
- </Tooltip>
58
- </div>
59
- );
60
- };
@@ -1,179 +0,0 @@
1
- import { useState } from 'react';
2
- import { observer } from 'mobx-react-lite';
3
- import Icon from '@components/Icon';
4
- import { Button, Tooltip, Popover, Segmented, ConfigProvider, Popconfirm } from 'antd';
5
- import stores from '@stores';
6
- import { toDownloadFile, nanoid, modKey } from '@utils/utils';
7
- import useKeyboardShortcuts from '@hooks/useKeyboardShortcuts';
8
-
9
- export default observer(() => {
10
- const [loading, setLoading] = useState(false);
11
- const [open, setOpen] = useState(false);
12
- const [format, setFormat] = useState('png');
13
- const [ratio, setRatio] = useState(1);
14
- const handleOpenChange = (newOpen) => {
15
- setOpen(newOpen);
16
- };
17
- const toDownload = async () => {
18
- if (!stores.editor.isEditing) return;
19
- if (loading) return;
20
- const option = {
21
- pixelRatio: ratio
22
- };
23
- if (['jpg', 'webp'].includes(format)) {
24
- option.quality = 0.9;
25
- option.fill = '#ffffff';
26
- }
27
- const key = nanoid();
28
- setLoading(true);
29
- stores.editor.message.open({
30
- key,
31
- type: 'loading',
32
- content: 'Downloading...',
33
- });
34
- await stores.editor.app.tree.export(format, option).then(result => {
35
- let name = `ShotEasy`;
36
- if (ratio > 1) name += `@${ ratio }`;
37
- toDownloadFile(result.data, `${ name }.${ format }`);
38
- stores.editor.message.open({
39
- key,
40
- type: 'success',
41
- content: 'Download Success!',
42
- });
43
- }).catch(() => {
44
- stores.editor.message.open({
45
- key,
46
- type: 'error',
47
- content: 'Download failed!',
48
- });
49
- })
50
- setLoading(false);
51
- };
52
- const toCopy = async () => {
53
- if (!stores.editor.isEditing) return;
54
- if (loading) return;
55
- const key = nanoid();
56
- setLoading(true);
57
- stores.editor.message.open({
58
- key,
59
- type: 'loading',
60
- content: 'Copying...',
61
- });
62
- await stores.editor.app.tree.export('png', { blob: true, pixelRatio: ratio }).then(async result => {
63
- const { data } = result;
64
- await navigator.clipboard.write([
65
- new ClipboardItem({
66
- [data.type]: data,
67
- }),
68
- ]);
69
- stores.editor.message.open({
70
- key,
71
- type: 'success',
72
- content: 'Copy Success!',
73
- });
74
- }).catch(() => {
75
- stores.editor.message.open({
76
- key,
77
- type: 'error',
78
- content: 'Copy failed!',
79
- });
80
- });
81
- setLoading(false);
82
- }
83
- const confirm = () => {
84
- stores.editor.clearImg();
85
- }
86
- useKeyboardShortcuts(() => toDownload(), () => toCopy(), [toDownload, toCopy]);
87
- const content = (<div>
88
- <div className="p-2 [&_.ant-segmented]:w-full [&_.ant-segmented-item]:w-[33%]">
89
- <div className="text-xs text-gray-400 mb-2">Format</div>
90
- <Segmented
91
- options={['png', 'jpg' , 'webp']}
92
- size="middle"
93
- onChange={setFormat}
94
- />
95
- <div className="text-xs text-gray-400 mt-2 mb-2">Pixel Ratio</div>
96
- <Segmented
97
- options={[{value: 1, icon: '1x'},{value: 2, icon: '2x'},{value: 3, icon: '3x'}]}
98
- size="middle"
99
- onChange={setRatio}
100
- />
101
- {stores.option.frameConf.width &&
102
- <div className="text-xs p-3 mt-4 flex justify-between bg-black/5 rounded-md">
103
- <span className="text-gray-400">Download Size</span>
104
- <span className="text-gray-700">{stores.option.frameConf.width * ratio} x {stores.option.frameConf.height * ratio}</span>
105
- </div>
106
- }
107
- </div>
108
- </div>)
109
- return (
110
- <div className='shrink-0 py-4 px-6 flex gap-2 justify-center items-center'>
111
- <ConfigProvider
112
- theme={{
113
- components: {
114
- Button: {
115
- colorPrimary: '#000',
116
- algorithm: true, // 启用算法
117
- },
118
- },
119
- }}
120
- >
121
- <div className='ant-space-compact flex flex-1'>
122
- <Tooltip placement='top' title={<span>Download {modKey} + S</span>}>
123
- <Button
124
- type='primary'
125
- size='large'
126
- loading={loading}
127
- icon={<Icon.ImageDown size={18} />}
128
- className='rounded-se-none flex-1 rounded-ee-none me-[-1px] hover:z-[1] border-r-white/30'
129
- onClick={toDownload}
130
- >
131
- <div className='leading-4 px-2'>
132
- <div className='text-sm leading-4 font-semibold'>
133
- Download
134
- </div>
135
- <div className='text-xs'>{ratio}x as {format.toLocaleUpperCase()}</div>
136
- </div>
137
- </Button>
138
- </Tooltip>
139
- <Tooltip placement='top' title={<span>Copy {modKey} + C</span>}>
140
- <Button
141
- type='primary'
142
- size='large'
143
- icon={<Icon.Copy size={18} />}
144
- loading={loading}
145
- className='rounded-ss-none rounded-es-none border-l-white/30'
146
- onClick={toCopy}
147
- />
148
- </Tooltip>
149
- </div>
150
- </ConfigProvider>
151
- <div className="flex items-center gap-1">
152
- <Popover
153
- content={content}
154
- trigger='click'
155
- arrow={false}
156
- placement="topRight"
157
- open={open}
158
- overlayStyle={{
159
- width: '320px',
160
- }}
161
- onOpenChange={handleOpenChange}
162
- >
163
- <Button size='large' icon={<Icon.Settings2 size={18} />} />
164
- </Popover>
165
- {stores.editor.img?.src &&
166
- <Popconfirm
167
- title="Delete the screenshot"
168
- description="Are you sure to delete this screenshot?"
169
- onConfirm={confirm}
170
- okText="Yes"
171
- cancelText="No"
172
- >
173
- <Button size='large' icon={<Icon.Trash2 size={18} />} />
174
- </Popconfirm>
175
- }
176
- </div>
177
- </div>
178
- );
179
- });
@@ -1,64 +0,0 @@
1
- import { Button, Drawer } from 'antd';
2
- import { observer } from 'mobx-react-lite';
3
- import Icon from '@components/Icon';
4
- import ColorPicker from '@components/ColorPicker';
5
- import stores from '@stores';
6
- import colorSvg from '@assets/color.svg';
7
- import { BackgroundSelect } from './BackgroundSelect';
8
-
9
- export default observer(({ showMore, onChange }) => {
10
- const onMoreClose = () => {
11
- onChange(false);
12
- }
13
- const handleCustom = (e) => {
14
- const color = e.toHexString();
15
- stores.option.frameConf.background = {
16
- type: 'solid',
17
- color
18
- }
19
- }
20
- const onSelectChange = (key) => {
21
- stores.option.setBackground(key);
22
- }
23
- return (
24
- <Drawer
25
- title=""
26
- placement="right"
27
- closable={false}
28
- mask={false}
29
- onClose={onMoreClose}
30
- open={showMore}
31
- getContainer={false}
32
- width="100%"
33
- className="[&_.ant-drawer-body]:p-0"
34
- >
35
- <div className="flex flex-col gap-2 h-full overflow-hidden">
36
- <div className="shrink-0 pt-4 px-4">
37
- <Button
38
- type="text"
39
- size="small"
40
- className="text-xs flex items-center opacity-80 m-0"
41
- icon={<Icon.ChevronLeft size={16} />}
42
- onClick={() => onChange(false)}
43
- >Back</Button>
44
- </div>
45
- <div className="h-0 flex-1 overflow-y-auto px-4 py-2">
46
- <h4 className="text-sm font-bold py-2">Custom</h4>
47
- <div className="py-1">
48
- <ColorPicker onChange={handleCustom}>
49
- <Button type="default" size="small" shape="circle" icon={<img src={colorSvg} width={18} />} />
50
- </ColorPicker>
51
- </div>
52
- <h4 className="text-sm font-bold py-2">Solid Colors</h4>
53
- <BackgroundSelect type="solid" onChange={onSelectChange} value={stores.option.background} />
54
- <h4 className="text-sm font-bold py-2">Gradients</h4>
55
- <BackgroundSelect type="gradient" onChange={onSelectChange} value={stores.option.background} />
56
- <h4 className="text-sm font-bold py-2">Cosmic Gradients</h4>
57
- <BackgroundSelect type="cosmic" onChange={onSelectChange} value={stores.option.background} />
58
- <h4 className="text-sm font-bold py-2">Desktop</h4>
59
- <BackgroundSelect type="desktop" onChange={onSelectChange} value={stores.option.background} />
60
- </div>
61
- </div>
62
- </Drawer>
63
- )
64
- });
@@ -1,45 +0,0 @@
1
- import { useState } from 'react';
2
- import { observer } from 'mobx-react-lite';
3
- import Icon from '@components/Icon';
4
- import { Button, Popover } from 'antd';
5
- import { cn } from '@utils/utils';
6
- import stores from '@stores';
7
-
8
- const cols = ['top-left', 'top', 'top-right', 'left', 'center', 'right', 'bottom-left', 'bottom', 'bottom-right'];
9
-
10
- export default observer(() => {
11
- const [open, setOpen] = useState(false);
12
- const hide = () => {
13
- setOpen(false);
14
- };
15
- const handleOpenChange = (newOpen) => {
16
- setOpen(newOpen);
17
- };
18
- const handleSelect = (value) => {
19
- stores.option.setAlign(value);
20
- }
21
- const content = (
22
- <div className={cn("flex flex-wrap w-24 position-block", stores.option.align)}>
23
- {cols.map(item => (
24
- <div key={item} className="w-8 h-8 border border-gray-200 rounded-sm hover:bg-gray-100 cursor-pointer" onClick={() => handleSelect(item)}></div>
25
- ))}
26
- </div>
27
- )
28
- return (
29
- <Popover
30
- content={content}
31
- trigger='click'
32
- arrow={false}
33
- placement="bottomRight"
34
- open={open}
35
- onOpenChange={handleOpenChange}
36
- >
37
- <Button
38
- type='text'
39
- shape='circle'
40
- className={cn(open && "shadow-md")}
41
- icon={<Icon.LayoutGrid size={18} />}
42
- ></Button>
43
- </Popover>
44
- )
45
- })
@@ -1,131 +0,0 @@
1
- import { useState } from 'react';
2
- import { observer } from 'mobx-react-lite';
3
- import Icon from '@components/Icon';
4
- import { Button, Slider, Radio } from 'antd';
5
- import ColorPicker from '@components/ColorPicker';
6
- import stores from '@stores';
7
- import backgroundConfig from '@utils/backgroundConfig';
8
- import { cn } from '@utils/utils';
9
- import SizeBar from './SizeBar';
10
- import CropperImage from './CropperImage';
11
- import Position from './Position';
12
- import Watermark from './Watermark';
13
- import DownloadBar from './DownloadBar';
14
- import DrawerBar from './DrawerBar';
15
-
16
-
17
- export default observer(() => {
18
- const [showMore, setShowMore] = useState(false);
19
- const onBgChange = (e) => {
20
- const key = e.target.value;
21
- stores.option.setBackground(key);
22
- }
23
- return (
24
- <div className="bg-white flex flex-col md:w-[340px] border-l border-l-gray-50 shadow-lg relative z-10 select-none">
25
- <div className="flex-1 flex-col gap-2 p-4 overflow-y-auto overflow-x-hidden">
26
- <SizeBar />
27
- <div className="[&_label]:font-semibold pt-2 [&_label]:text-sm">
28
- <label>Quick</label>
29
- <div className="flex gap-4 items-center py-2">
30
- <CropperImage />
31
- <Button
32
- type='text'
33
- shape='circle'
34
- onClick={() => stores.option.toggleFlip('x')}
35
- icon={<Icon.FlipHorizontal2 size={18} />}
36
- ></Button>
37
- <Button
38
- type='text'
39
- shape='circle'
40
- onClick={() => stores.option.toggleFlip('y')}
41
- icon={<Icon.FlipVertical2 size={18} />}
42
- ></Button>
43
- <Position />
44
- {/* Todo */}
45
- {/* <Button
46
- type='text'
47
- shape='circle'
48
- icon={<Icon.Box size={18} />}
49
- ></Button> */}
50
- {/* <Button
51
- type='text'
52
- shape='circle'
53
- icon={<Icon.Sunset size={18} />}
54
- ></Button> */}
55
- </div>
56
- </div>
57
- <div className="[&_label]:font-semibold [&_label]:text-sm">
58
- <label>Scale</label>
59
- <Slider
60
- min={0.1}
61
- max={2}
62
- step={0.1}
63
- onChange={(e) => stores.option.setScale(e)}
64
- value={typeof stores.option.scale === 'number' ? stores.option.scale : 1}
65
- />
66
- </div>
67
- <div className="[&_label]:font-semibold [&_label]:text-sm">
68
- <div className="flex justify-between">
69
- <label>Padding</label>
70
- <ColorPicker value={stores.option.paddingBg} onChange={(e) => stores.option.setPaddingBg(e.toRgbString())} size="small" />
71
- </div>
72
- <Slider
73
- min={0}
74
- max={60}
75
- onChange={(e) => stores.option.setPadding(e)}
76
- value={typeof stores.option.padding === 'number' ? stores.option.padding : 0}
77
- />
78
- </div>
79
- <div className="[&_label]:font-semibold [&_label]:text-sm">
80
- <label>Rounded</label>
81
- <Slider
82
- min={0}
83
- max={20}
84
- onChange={(e) => stores.option.setRound(e)}
85
- value={typeof stores.option.round === 'number' ? stores.option.round : 0}
86
- />
87
- </div>
88
- <div className="[&_label]:font-semibold [&_label]:text-sm">
89
- <label>Shadow</label>
90
- <Slider
91
- min={0}
92
- max={6}
93
- onChange={(e) => stores.option.setShadow(e)}
94
- value={typeof stores.option.shadow === 'number' ? stores.option.shadow : 0}
95
- />
96
- </div>
97
- <div className="[&_label]:font-semibold [&_label]:text-sm">
98
- <div className="flex justify-between items-center">
99
- <label>Background</label>
100
- <Button
101
- type="text"
102
- size="small"
103
- className="text-xs flex items-center opacity-80 m-0"
104
- onClick={() => setShowMore(true)}
105
- >More <Icon.ChevronRight size={16} /></Button>
106
- </div>
107
- <div className="py-3">
108
- <Radio.Group
109
- onChange={onBgChange} value={stores.option.background}
110
- rootClassName="grid grid-cols-7 [&_span]:ps-0"
111
- >
112
- <Radio className="[&_.ant-radio]:hidden [&_span]:p-0 mr-0" value='default_1'>
113
- <div className={cn("w-8 h-8 rounded-full", backgroundConfig.default_1.class)}></div>
114
- </Radio>
115
- {Object.keys(backgroundConfig).map((key) => {
116
- if (key.includes('default') && key !== 'default_1') return (
117
- <Radio key={key} className="[&_.ant-radio]:hidden [&_span]:p-0 mr-0" value={key}>
118
- <div className={cn("w-8 h-8 rounded-full", backgroundConfig[key].class)}></div>
119
- </Radio>
120
- )
121
- })}
122
- </Radio.Group>
123
- </div>
124
- </div>
125
- <Watermark />
126
- </div>
127
- <DownloadBar />
128
- <DrawerBar showMore={showMore} onChange={setShowMore} />
129
- </div>
130
- )
131
- });