image-beautifier 1.0.0
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.
- package/.eslintrc.cjs +25 -0
- package/LICENSE +21 -0
- package/README.md +28 -0
- package/favicon.svg +20 -0
- package/index.html +13 -0
- package/jsconfig.json +12 -0
- package/package.json +53 -0
- package/postcss.config.mjs +6 -0
- package/preview.png +0 -0
- package/public/vite.svg +1 -0
- package/src/App.jsx +29 -0
- package/src/assets/blur.svg +8 -0
- package/src/assets/color.svg +1 -0
- package/src/assets/demo.png +0 -0
- package/src/assets/pencil.png +0 -0
- package/src/assets/rotate.png +0 -0
- package/src/components/ColorPicker.jsx +27 -0
- package/src/components/Icon.jsx +81 -0
- package/src/components/editor/Editor.jsx +19 -0
- package/src/components/editor/HotKeys.jsx +48 -0
- package/src/components/editor/View.jsx +167 -0
- package/src/components/editor/Zoom.jsx +54 -0
- package/src/components/editor/layers/FrameBox.jsx +51 -0
- package/src/components/editor/layers/Screenshot.jsx +89 -0
- package/src/components/editor/layers/ShapeLine.jsx +97 -0
- package/src/components/editor/layers/Watermark.jsx +41 -0
- package/src/components/header/EmojiSelect.jsx +35 -0
- package/src/components/header/Header.jsx +134 -0
- package/src/components/header/Logo.jsx +29 -0
- package/src/components/header/MediaLogo.jsx +10 -0
- package/src/components/header/WidthDropdown.jsx +74 -0
- package/src/components/init/Init.jsx +77 -0
- package/src/components/sideBar/BackgroundSelect.jsx +49 -0
- package/src/components/sideBar/CropperImage.jsx +73 -0
- package/src/components/sideBar/CustomSize.jsx +60 -0
- package/src/components/sideBar/DownloadBar.jsx +179 -0
- package/src/components/sideBar/DrawerBar.jsx +64 -0
- package/src/components/sideBar/Position.jsx +45 -0
- package/src/components/sideBar/SideBar.jsx +131 -0
- package/src/components/sideBar/SizeBar.jsx +114 -0
- package/src/components/sideBar/Watermark.jsx +59 -0
- package/src/hooks/useKeyboardShortcuts.js +28 -0
- package/src/hooks/usePaste.js +21 -0
- package/src/index.js +1 -0
- package/src/main.jsx +9 -0
- package/src/stores/editor.js +106 -0
- package/src/stores/index.js +7 -0
- package/src/stores/option.js +96 -0
- package/src/style/main.css +132 -0
- package/src/utils/UndoRedoManager.js +83 -0
- package/src/utils/backgroundConfig.js +387 -0
- package/src/utils/captureScreen.js +28 -0
- package/src/utils/sizeConfig.js +163 -0
- package/src/utils/utils.js +154 -0
- package/tailwind.config.mjs +15 -0
- package/vite.config.js +21 -0
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
});
|