├── .travis.yml
├── src
├── .eslintrc
├── styles.module.css
├── config.js
├── constants.js
└── index.js
├── .eslintignore
├── .editorconfig
├── .prettierrc
├── .npmignore
├── .gitignore
├── .eslintrc
├── package.json
└── README.md
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 12
4 | - 10
5 |
--------------------------------------------------------------------------------
/src/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "jest": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/
2 | dist/
3 | node_modules/
4 | .snapshots/
5 | *.min.js
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/src/styles.module.css:
--------------------------------------------------------------------------------
1 | /* add css module styles here (optional) */
2 |
3 | .test {
4 | margin: 2em;
5 | padding: 0.5em;
6 | border: 2px solid #000;
7 | font-size: 2em;
8 | text-align: center;
9 | }
10 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "jsxSingleQuote": true,
4 | "semi": false,
5 | "tabWidth": 2,
6 | "bracketSpacing": true,
7 | "jsxBracketSameLine": false,
8 | "arrowParens": "always",
9 | "trailingComma": "none"
10 | }
11 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | example
2 | ## the src folder
3 | src
4 | .babelrc
5 | rollup.config.js
6 | ## node modules folder
7 | node_modules
8 | ## git repository related files
9 | .git
10 | .gitignore
11 | CVS
12 | .svn
13 | .hg
14 | .lock-wscript
15 | .wafpickle-N
16 | .DS_Store
17 | npm-debug.log
18 | .npmrc
19 | #others
20 | config.gypi
21 | package-lock.json
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # See https://help.github.com/ignore-files/ for more about ignoring files.
3 |
4 | example
5 | src/index.test.js
6 |
7 | # dependencies
8 | node_modules
9 |
10 | # builds
11 | build
12 | dist
13 | .rpt2_cache
14 |
15 | # misc
16 | .DS_Store
17 | .env
18 | .env.local
19 | .env.development.local
20 | .env.test.local
21 | .env.production.local
22 |
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | /.idea
27 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": [
4 | "standard",
5 | "standard-react",
6 | "plugin:prettier/recommended",
7 | "prettier/standard",
8 | "prettier/react"
9 | ],
10 | "env": {
11 | "node": true
12 | },
13 | "parserOptions": {
14 | "ecmaVersion": 2020,
15 | "ecmaFeatures": {
16 | "legacyDecorators": true,
17 | "jsx": true
18 | }
19 | },
20 | "settings": {
21 | "react": {
22 | "version": "16"
23 | }
24 | },
25 | "rules": {
26 | "space-before-function-paren": 0,
27 | "react/prop-types": 0,
28 | "react/jsx-handler-names": 0,
29 | "react/jsx-fragments": 0,
30 | "react/no-unused-prop-types": 0,
31 | "import/export": 0
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | const Config = {
2 |
3 | //Initial cropping offsets
4 |
5 | INIT_X1: 20,
6 | INIT_Y1: 20,
7 | INIT_X2: 80,
8 | INIT_Y2: 80,
9 |
10 | // Max crop offest (should be in percentage between 1 and 100)
11 |
12 | MAX_CROP: 50,
13 |
14 | // Edge sensitivity
15 |
16 | CROP_EDGE_SENSITIVITY:20,
17 |
18 | // Screen flows
19 |
20 | FLOW_INIT : 0,
21 | FLOW_IMG_CHOSEN : 1,
22 | FLOW_CROP : 2,
23 | FLOW_PREVIEW: 3,
24 | FLOW_UPLOAD: 4,
25 | FLOW_SUCESS: 5,
26 | FLOW_ERROR: 6,
27 | FLOW_PDF_CHOSEN: 7,
28 | FLOW_PDF_PREVIEW: 8,
29 | FLOW_VIDEO_CHOSEN: 9,
30 | FLOW_VIDEO_PREVIEW: 10,
31 | FLOW_VIDEO_PROCESSING: 11,
32 |
33 | // Screen sub flows
34 |
35 | FLOW_VIDEO_PREVIEW_INIT : 0,
36 | FLOW_VIDEO_PREVIEW_THUMBNAIL_VIEW : 1,
37 |
38 |
39 | THUMBNAIL_WIDTH: 60,
40 |
41 |
42 | }
43 |
44 | export {Config};
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | const Constants = {
2 |
3 | TYPE_IMAGE: "image",
4 | TYPE_PDF: 'pdf',
5 | TYPE_VIDEO: 'video',
6 |
7 | JOB_STATUS_SUBMITTED: "SUBMITTED",
8 | JOB_STATUS_COMPLETE: "COMPLETE",
9 |
10 | TITLE_FLOW_INIT: 'Choose a file to begin upload',
11 | TITLE_FLOW_IMAGE_CHOSEN: "Image chosen",
12 | TITLE_FLOW_CROP: "Drag the edges to crop",
13 | TITLE_FLOW_PREVIEW: "Image Preview",
14 | TITLE_FLOW_UPLOAD: "Uploading",
15 | TITLE_FLOW_SUCCESS: "Upload Successful",
16 | TITLE_FLOW_ERROR: "Upload Failed",
17 | TITLE_FLOW_PDF_CHOSEN: "PDF Selected",
18 | TITLE_FLOW_PDF_PREVIEW: "PDF Preview",
19 | TITLE_FLOW_VIDEO_CHOSEN: "Video Selected",
20 | TITLE_FLOW_VIDEO_PREVIEW: "Video Preview",
21 | TITLE_FLOW_VIDEO_PROCESSING: "Video Processing",
22 |
23 | HINT_FLOW_INIT: 'are supported',
24 | HINT_FLOW_IMAGE_CHOSEN: "You can crop it before uploading",
25 | HINT_FLOW_CROP: "Drag in the center to move the entire cropped view",
26 | HINT_FLOW_PREVIEW: "You can go back and re-crop if you wish",
27 | HINT_FLOW_UPLOAD: "Please wait, uploading in progress...",
28 | HINT_FLOW_SUCCESS: "Your file upload is complete",
29 | HINT_FLOW_ERROR: "Your file upload could not complete",
30 | HINT_FLOW_PDF_CHOSEN: "You can preview it before uploading",
31 | HINT_FLOW_PDF_PREVIEW: "Upload the file if preview is fine",
32 | HINT_FLOW_VIDEO_CHOSEN: "You can preview it before uploading",
33 | HINT_FLOW_VIDEO_PREVIEW: "Upload the file if preview is fine, else clip it as you wish. To clip, drag the video to the desired start / end position and press on the mark start / end buttons.",
34 | HINT_FLOW_VIDEO_PROCESSING: "Video is processing. Please wait, it will be ready shortly.",
35 |
36 | }
37 |
38 | export {Constants};
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-upload-to-s3",
3 | "version": "1.0.17",
4 | "description": "A react component for uploading files to AWS S3",
5 | "author": "superflows-dev",
6 | "license": "MIT",
7 | "repository": "superflows-dev/react-upload-to-s3",
8 | "main": "dist/index.js",
9 | "module": "dist/index.modern.js",
10 | "source": "src/index.js",
11 | "engines": {
12 | "node": ">=10"
13 | },
14 | "scripts": {
15 | "build": "microbundle-crl --no-compress --format modern,cjs",
16 | "start": "microbundle-crl watch --no-compress --format modern,cjs",
17 | "prepare": "run-s build",
18 | "test": "run-s test:unit",
19 | "test:build": "run-s build",
20 | "test:lint": "eslint .",
21 | "test:unit": "cross-env CI=1 react-scripts test --env=jsdom --verbose=true --coverage",
22 | "test:watch": "react-scripts test --env=jsdom",
23 | "predeploy": "cd example && npm install && npm run build",
24 | "deploy": "gh-pages -d example/build"
25 | },
26 | "peerDependencies": {
27 | "react": "^16.0.0||^17.0.0||^18.0.0"
28 | },
29 | "devDependencies": {
30 | "@testing-library/jest-dom": "^5.16.5",
31 | "@testing-library/react": "12.1.2",
32 | "babel-eslint": "^10.0.3",
33 | "cross-env": "^7.0.3",
34 | "eslint": "^6.8.0",
35 | "eslint-config-prettier": "^6.15.0",
36 | "eslint-config-standard": "^14.1.1",
37 | "eslint-config-standard-react": "^9.2.0",
38 | "eslint-plugin-import": "^2.26.0",
39 | "eslint-plugin-node": "^11.1.0",
40 | "eslint-plugin-prettier": "^3.4.1",
41 | "eslint-plugin-promise": "^4.3.1",
42 | "eslint-plugin-react": "^7.30.1",
43 | "eslint-plugin-standard": "^4.1.0",
44 | "gh-pages": "^2.2.0",
45 | "microbundle-crl": "^0.13.11",
46 | "npm-run-all": "^4.1.5",
47 | "prettier": "^2.7.1",
48 | "react": "^16.14.0",
49 | "react-dom": "^16.14.0",
50 | "react-scripts": "^3.4.4"
51 | },
52 | "files": [
53 | "dist"
54 | ],
55 | "dependencies": {
56 | "aws-sdk": "^2.1191.0",
57 | "bootstrap": "^5.2.0",
58 | "react-bootstrap": "^2.5.0",
59 | "react-bootstrap-icons": "^1.8.4",
60 | "react-ui-components-superflows": "^1.0.29",
61 | "react-ui-themes-superflows": "^1.0.13"
62 | },
63 | "keywords": [
64 | "react",
65 | "superflows",
66 | "aws",
67 | "s3",
68 | "upload",
69 | "uploader",
70 | "post",
71 | "image",
72 | "pdf",
73 | "video",
74 | "hls",
75 | "mediaconvert"
76 | ]
77 | }
78 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-upload-to-s3
2 |
3 | > The all-in-one react-only component for uploading images, documents and videos to AWS S3. This is a pure front-end component and only requires AWS configuration at the backend, no backend code is necessary.
4 |
5 | [](https://www.npmjs.com/package/react-upload-to-s3) [](https://standardjs.com)
6 |
7 | ## What's New
8 |
9 | - Thumbnail auto generation and capture feature
10 |
11 | ## How it can be used
12 |
13 | ### Upload an image to S3 (with easy crop)
14 |
15 |
16 | - Choose an image
17 | - Preview it
18 | - Crop it if required
19 | - Upload to S3
20 |
21 |
22 |
23 | ### Upload a video to S3 (with thumbnail generator & easy clip)
24 |
25 |
26 | - Choose a video
27 | - Preview the video and the generated thumbnail
28 | - Capture a new thumbnail if required
29 | - Clip the video from start and end if required
30 | - Upload to S3
31 |
32 |
33 |
34 | ### Upload a pdf to S3
35 |
36 |
37 | - Choose a pdf
38 | - Preview it
39 | - Upload to S3
40 |
41 |
42 |
43 | ## Demo
44 |
45 | [](https://stackblitz.com/edit/react-ts-kz4eqr?file=App.tsx)
46 |
47 | ## Install
48 |
49 | ```bash
50 | npm install --save react-upload-to-s3
51 | ```
52 | Then install the dependencies
53 |
54 | ## Dependencies
55 |
56 | ```bash
57 | npm install --save aws-sdk
58 | npm install --save bootstrap
59 | npm install --save react-bootstrap
60 | npm install --save react-bootstrap-icons
61 | npm install --save react-ui-components-superflows
62 | npm install --save react-ui-themes-superflows
63 | ```
64 |
65 | ## Usage
66 |
67 | [](https://stackblitz.com/edit/react-ts-kz4eqr?file=App.tsx)
68 |
69 | ### Props
70 |
71 | - bucket: Name of the S3 bucket
72 | - cognitoIdentityCredentials: Cognito Identity Pool Object
73 | - awsRegion: Region where the bucket exists
74 | - awsKey: AWS Access Key (should come from environment variables)
75 | - awsSecret: AWS Secret (should come from environment variables)
76 | - awsMediaConvertEndPoint: AWS region specific mediaconvert endpoint
77 | - type: can be image / video / pdf
78 | - mediaConvertRole: Media convert role
79 | - onResult: Result callback
80 | - theme: UI Theme (optional)
81 | - showNewUpload: Flag which enables the display of New Upload button on the success screen (optional, default value is true)
82 |
83 |
84 | ### Usage Method 1 - Less Secure
85 |
86 | ```jsx
87 |
88 | import React from 'react'
89 |
90 | import Themes from 'react-ui-themes-superflows'
91 | import { Col, Row, Container } from 'react-bootstrap';
92 | import { UploadToS3 } from 'react-upload-to-s3'
93 | import 'bootstrap/dist/css/bootstrap.min.css';
94 |
95 | const App = () => {
96 |
97 | const theme = Themes.getTheme("Default");
98 |
99 | return (
100 |
101 |
102 |
103 |
104 | {console.log('on Result', result);}} />
116 |
117 |
118 |
119 |
120 | );
121 |
122 | }
123 |
124 | export default App
125 |
126 | ```
127 |
128 | ### Usage Method 2 - More Secure
129 |
130 | ```jsx
131 |
132 | import React from 'react'
133 |
134 | import Themes from 'react-ui-themes-superflows'
135 | import { Col, Row, Container } from 'react-bootstrap';
136 | import { UploadToS3 } from 'react-upload-to-s3'
137 | import 'bootstrap/dist/css/bootstrap.min.css';
138 |
139 | const App = () => {
140 |
141 | const theme = Themes.getTheme("Default");
142 |
143 | return (
144 |
145 |
146 |
147 |
148 | {console.log('on Result', result);}} />
163 |
164 |
165 |
166 |
167 | );
168 |
169 | }
170 |
171 | export default App
172 |
173 | ```
174 |
175 | ## Configuration
176 |
177 | ### AWS S3
178 |
179 | - Create an S3 bucket via the AWS admin console, say name of the bucket is **myuploads**
180 | - Set the bucket policy as follows
181 | ```bash
182 | {
183 | "Version": "2012-10-17",
184 | "Statement": [
185 | {
186 | "Sid": "PublicListPutGet",
187 | "Effect": "Allow",
188 | "Principal": "*",
189 | "Action": [
190 | "s3:List*",
191 | "s3:Put*",
192 | "s3:Get*"
193 | ],
194 | "Resource": [
195 | "arn:aws:s3:::myuploads",
196 | "arn:aws:s3:::myuploads/*"
197 | ]
198 | }
199 | ]
200 | }
201 | ```
202 | - Set the cors policy as follows
203 | ```bash
204 | [
205 | {
206 | "AllowedHeaders": [
207 | "*"
208 | ],
209 | "AllowedMethods": [
210 | "PUT",
211 | "POST",
212 | "DELETE",
213 | "GET"
214 | ],
215 | "AllowedOrigins": [
216 | "*"
217 | ],
218 | "ExposeHeaders": []
219 | }
220 | ]
221 | ```
222 |
223 | ### AWS MediaConvert
224 |
225 | AWS mediaconvert is required for video processing. The clip selection happens at the client end, whereas the actual clipping is done by an AWS mediaconvert job. This requires a region specific endpoint and can be easily obtained from the aws cli (aws commandline).
226 |
227 | ```bash
228 | aws mediaconvert describe-endpoints --region
229 | ```
230 |
231 | Remember that this region specific endpoint also has to be provided as a prop to the upload-to-s3 component. (Refer to the Usage Section)
232 |
233 | You will also have to create a mediaconvert role.
234 |
235 | #### MediaConvert Role
236 |
237 | - Goto IAM > Roles
238 | - Select AWS Service as the trusted entity type
239 | - Choose MediaConvert from the services dropdown
240 | - Click next on add permissions & attach the following permissions to it - (1) Full access to the particular s3 bucket, (2) Access to the region specific endpoint of the API gateway
241 | - Name the role as per your choice. I have named it **mediaconvert_role**. (Remember that this role name has to be given as a prop to the upload-to-s3 component, refer to the Usage section)
242 |
243 | ### Authentication of AWS SDK
244 |
245 | #### Method 1 - Pass Credentials Via Props (Less Secure)
246 |
247 | - Create an SDK user via the AWS console so that you get access to aws region, aws access key and aws secret, i.e. aws credentials.
248 | - Ensure that you preserve these credentials in a secure manner.
249 | - It is especially important that these credentials be stored in the environment files and should never be pushed to a source repository such as git.
250 | - For this SDK user, give create, add, edit, delete permissions to your S3 bucket via the AWS console. I usually give full access restricted to a particular bucket, like the one which we created in the S3 section above (given below).
251 | - If you are planning to use this module for video upload, also provide permissions to elemental media convert (given below).
252 | - An additional permission needs to be given for video processing, for using the passrole method privilege (given below).
253 |
254 | #### Method 2 - Use AWS Cognito Federated Identities (Recommended Method)
255 |
256 | - Create a new identity pool using Cognito.
257 | - It will end up creating two roles, one for users authenticated via cognito and the second, for unauthenticated users
258 | - Go to Roles in IAM
259 | - If in your application, unauthenticated & authenticated users will be using this module, then you will need to give s3 and elemental mediaconvert permissions to both the roles.
260 | - Else, since this module will always be behind the authentication wall, you will need to give s3 and elemental mediaconvert permissions to only the authenticated users role.
261 | - For S3, a good idea would be to give full access restricted to the particular bucket (given below).
262 | - If you are planning to use this module for video upload, also provide permissions to elemental media convert (given below).
263 | - An additional permission is required for video processing for using the passrole method.
264 | - An additional permission needs to be given for video processing, for using the passrole method privilege (given below).
265 |
266 | #### S3 Permission (Needed For Both Methods)
267 |
268 | ```bash
269 |
270 | {
271 | "Version": "2012-10-17",
272 | "Statement": [
273 | {
274 | "Effect": "Allow",
275 | "Action": [
276 | "s3:*",
277 | "s3-object-lambda:*"
278 | ],
279 | "Resource": "arn:aws:s3:::myuploads"
280 | }
281 | ]
282 | }
283 |
284 | ```
285 |
286 | #### MediaConvert Permissions (Needed For Both Methods)
287 |
288 | - For this SDK user, then give the user access to AWS mediaconvert via the AWS console. I have used AWSElementalMediaConvertFullAccess, which is a pre-created AWS policy for this. To find and attach this policy - Select your IAM user > Click add permissions on the user summary screen > Click attach existing policies directly > Search mediaconvert > Apply the AWSElementalMediaConvertFullAccess policy
289 |
290 |
291 | #### Permission to Use PassRole For Video Processing (Needed For Both Methods)
292 |
293 | - Create a new inline policy (for method 1, attach to the user, for method 2, attach to the role(s)) with the following json
294 |
295 | ```bash
296 | {
297 | "Version": "2012-10-17",
298 | "Statement": [
299 | {
300 | "Sid": "VisualEditor0",
301 | "Effect": "Allow",
302 | "Action": [
303 | "iam:GetRole",
304 | "iam:PassRole"
305 | ],
306 | "Resource": "arn:aws:iam::mediaconvert_role_id:role/*"
307 | },
308 | {
309 | "Sid": "VisualEditor1",
310 | "Effect": "Allow",
311 | "Action": "mediaconvert:*",
312 | "Resource": "*"
313 | },
314 | {
315 | "Sid": "VisualEditor2",
316 | "Effect": "Allow",
317 | "Action": "iam:ListRoles",
318 | "Resource": "*"
319 | }
320 | ]
321 | }
322 |
323 | ```
324 |
325 |
326 | Once you are through with installing the dependencies and the AWS configuration, using the component becomes fairly simple. Please refer to the Usage above.
327 |
328 |
329 |
330 | ## License
331 |
332 | MIT © [superflows-dev](https://github.com/superflows-dev)
333 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import React, { useEffect } from 'react'
3 | import { useState, useRef } from 'react'
4 | import { Col, Row, Container, Button } from 'react-bootstrap';
5 | import { CloudArrowUp, Check2Circle, ExclamationCircle, FilePdf, FileEarmarkPlay, Scissors, BoxArrowLeft, BoxArrowRight, CameraFill, Eye } from 'react-bootstrap-icons';
6 | import * as Icons from 'react-bootstrap-icons';
7 | import { ButtonNeutral, ButtonNext } from 'react-ui-components-superflows';
8 | import Themes from 'react-ui-themes-superflows'
9 |
10 | import { Config } from './config';
11 | import { Constants } from './constants';
12 |
13 | import * as AWS from 'aws-sdk'
14 | import * as MediaConvert from "aws-sdk/clients/mediaconvert";
15 |
16 | import { ConfigurationOptions } from 'aws-sdk'
17 |
18 |
19 | function updateAWSConfigAndGetClient(cognitoIdentityCredentials, region, secret, key, endpoint) {
20 |
21 | if(cognitoIdentityCredentials != null) {
22 | AWS.config.region = region; // Region
23 | AWS.config.credentials = new AWS.CognitoIdentityCredentials(cognitoIdentityCredentials);
24 | } else {
25 | const configuration: ConfigurationOptions = {
26 | region: region,
27 | secretAccessKey: secret,
28 | accessKeyId: key
29 | }
30 | AWS.config.update(configuration)
31 | }
32 | AWS.config.mediaconvert = {endpoint : endpoint};
33 | return new AWS.DynamoDB.DocumentClient();
34 |
35 | }
36 |
37 | function getMyBucket(bucket, region) {
38 | const myBucket = new AWS.S3({
39 | params: { Bucket: bucket},
40 | region: region,
41 | })
42 | //console.logg(myBucket);
43 |
44 | return myBucket;
45 | }
46 |
47 | function getWindowDimensions() {
48 | const { innerWidth: width, innerHeight: height } = window;
49 | return {
50 | width,
51 | height
52 | };
53 | }
54 |
55 | export const UploadToS3 = (props) => {
56 |
57 | const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
58 | const [progress , setProgress] = useState(0);
59 | const [flow, setFlow] = useState(Config.FLOW_INIT)
60 | const [type, setType] = useState(Constants.TYPE_IMAGE);
61 | const [subFlow, setSubFlow] = useState(Config.FLOW_VIDEO_PREVIEW_INIT)
62 | const [moveX, setMoveX] = useState(0);
63 | const [moveY, setMoveY] = useState(0)
64 | const [tsX, setTsX] = useState(0);
65 | const [tsY, setTsY] = useState(0);
66 | const [teX, setTeX] = useState(0);
67 | const [teY, setTeY] = useState(0);
68 | const [cropInitX1, setCropInitX1] = useState(Config.INIT_X1);
69 | const [cropInitY1, setCropInitY1] = useState(Config.INIT_Y1);
70 | const [cropInitX2, setCropInitX2] = useState(Config.INIT_X2);
71 | const [cropInitY2, setCropInitY2] = useState(Config.INIT_Y2);
72 | const [fileType, setFileType] = useState('')
73 | const [jobStatus, setJobStatus] = useState('')
74 | const [ext, setExt] = useState('')
75 | const [src, setSrc] = useState('')
76 | const [srcThumbnail, setSrcThumbnail] = useState('')
77 | const [videoName, setVideoName] = useState(null);
78 | const [videoDuration, setVideoDuration] = useState(null);
79 | const [videoSize, setVideoSize] = useState(null);
80 | const [videoWidth, setVideoWidth] = useState(0);
81 | const [videoHeight, setVideoHeight] = useState(0);
82 | const [videoCurrentTime, setVideoCurrentTime] = useState(null);
83 | const [videoStartPosition, setVideoStartPosition] = useState(null);
84 | const [videoEndPosition, setVideoEndPosition] = useState(null);
85 | const [pdfName, setPdfName] = useState('')
86 | const [pdfSize, setPdfSize] = useState('')
87 | const [disableMarkStart, setDisableMarkStart] = useState(false);
88 | const [disableMarkEnd, setDisableMarkEnd] = useState(false);
89 |
90 | const { [props.icon]: Icon } = Icons
91 | const refInputImage = useRef(null);
92 | const refInputPdf = useRef(null);
93 | const refInputVideo= useRef(null);
94 | const refInputVideoPreview= useRef(null);
95 | const refCanvas = useRef(null);
96 | const refCanvasOverlay = useRef(null);
97 | const refCanvasContainer = useRef(null);
98 | const refCanvasThumbnail = useRef(null);
99 |
100 | const defaultTheme = Themes.getTheme('Default');
101 |
102 | updateAWSConfigAndGetClient(props.cognitoIdentityCredentials, props.awsRegion, props.awsSecret, props.awsKey, props.awsMediaConvertEndPoint);
103 |
104 | function dataURItoBlob(dataURI) {
105 | // var binary = atob(dataURI.split(',')[1]);
106 | // var array = [];
107 | // for(var i = 0; i < binary.length; i++) {
108 | // array.push(binary.charCodeAt(i));
109 | // }
110 | // return new Blob([new Uint8Array(array)], {type: 'image/jpeg'});
111 | // convert base64 to raw binary data held in a string
112 | const byteString = atob(dataURI.split(',')[1]);
113 |
114 | // separate out the mime component
115 | const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
116 |
117 | // write the bytes of the string to an ArrayBuffer
118 | const arrayBuffer = new ArrayBuffer(byteString.length);
119 | const _ia = new Uint8Array(arrayBuffer);
120 | for (let i = 0; i < byteString.length; i++) {
121 | _ia[i] = byteString.charCodeAt(i);
122 | }
123 |
124 | const dataView = new DataView(arrayBuffer);
125 | const blob = new Blob([dataView], {type: mimeString});
126 | return blob;
127 | }
128 |
129 | function onUploadClick() {
130 | //console.log('upload click');
131 | uploadFile(ext)
132 | setFlowWrap(Config.FLOW_UPLOAD);
133 | }
134 |
135 | function onNewUploadClick() {
136 | setFlowWrap(Config.FLOW_INIT);
137 | }
138 |
139 | function setFlowWrap(value) {
140 | setTimeout(() => { setFlow(value);}, 500);
141 | }
142 |
143 | function setSubFlowWrap(value) {
144 | setTimeout(() => { setSubFlow(value);}, 500);
145 | }
146 |
147 | function setProgressWrap(value) {
148 | setTimeout(() => { setProgress(value);}, 100);
149 | }
150 |
151 | function onPreviewClicked() {
152 |
153 | if(flow === Config.FLOW_CROP) {
154 | setFlowWrap(Config.FLOW_PREVIEW);
155 | }
156 |
157 | //console.logg('flow', flow);
158 |
159 | if(flow === Config.FLOW_PDF_CHOSEN) {
160 | setFlowWrap(Config.FLOW_PDF_PREVIEW);
161 | }
162 |
163 | if(flow === Config.FLOW_VIDEO_CHOSEN) {
164 | setFlowWrap(Config.FLOW_VIDEO_PREVIEW);
165 | }
166 |
167 | }
168 |
169 | function onCancelClicked() {
170 |
171 | //console.logg('on cancel', flow);
172 |
173 | if(flow === Config.FLOW_IMG_CHOSEN || flow === Config.FLOW_PDF_CHOSEN || flow == Config.FLOW_VIDEO_CHOSEN) {
174 | setFlowWrap(Config.FLOW_INIT)
175 | clearInputs();
176 | }
177 |
178 | if(flow === Config.FLOW_CROP) {
179 | setFlowWrap(Config.FLOW_IMG_CHOSEN);
180 | }
181 |
182 | if(flow === Config.FLOW_PREVIEW) {
183 | setFlowWrap(Config.FLOW_CROP);
184 | }
185 |
186 | if(flow === Config.FLOW_PDF_PREVIEW) {
187 | setFlowWrap(Config.FLOW_PDF_CHOSEN);
188 | }
189 |
190 | if(flow === Config.FLOW_VIDEO_PREVIEW) {
191 | setFlowWrap(Config.FLOW_VIDEO_CHOSEN);
192 | }
193 |
194 | }
195 |
196 | function onCropClicked() {
197 | if(flow === Config.FLOW_IMG_CHOSEN) {
198 | setFlowWrap(Config.FLOW_CROP);
199 | }
200 | }
201 |
202 | function onTimeUpdate() {
203 | //console.log('currentime', refInputVideoPreview.current.currentTime);
204 | setVideoCurrentTime(parseInt(refInputVideoPreview.current.currentTime));
205 |
206 | setDisableMarkStart(false);
207 | setDisableMarkEnd(false);
208 |
209 | if(parseInt(videoStartPosition) > 0 || parseInt(videoEndPosition)) {
210 | if(parseInt(videoStartPosition) > 0) {
211 | if(parseInt(refInputVideoPreview.current.currentTime) < parseInt(videoStartPosition)) {
212 | setDisableMarkEnd(true);
213 | }
214 | }
215 | if(parseInt(videoEndPosition) > 0) {
216 | if(parseInt(refInputVideoPreview.current.currentTime) > parseInt(videoEndPosition)) {
217 | setDisableMarkStart(true);
218 | }
219 | }
220 | }
221 |
222 | }
223 |
224 | function onMarkStartPosition() {
225 | setVideoStartPosition(parseInt(videoCurrentTime));
226 | }
227 |
228 | function onMarkEndPosition() {
229 | setVideoEndPosition(parseInt(videoCurrentTime));
230 | }
231 |
232 | function gotoStartPosition() {
233 | refInputVideoPreview.current.currentTime = videoStartPosition
234 | }
235 |
236 | function gotoEndPosition() {
237 | refInputVideoPreview.current.currentTime = videoEndPosition
238 | }
239 |
240 | async function deleteOriginal(s3Input) {
241 |
242 | //console.logg(s3Input);
243 | //console.logg(props.bucket);
244 | //console.logg("s3://" + props.bucket);
245 | //console.logg(s3Input.replace(props.bucket + "/", ""));
246 |
247 | const fileName = s3Input.replace(props.bucket + "/", "").split(".")[0] + "." + ext;
248 |
249 | const params = {
250 | Bucket: props.bucket,
251 | Key: fileName,
252 | };
253 |
254 | //console.logg(params);
255 |
256 | const deletePromise = getMyBucket(props.bucket, props.awsRegion).deleteObject(params).promise();
257 | deletePromise.then(function(data) {
258 | //console.logg('delete result', data);
259 | });
260 |
261 | }
262 |
263 | function uploadThumbnail(url) {
264 | let blob = null;
265 | blob = dataURItoBlob(srcThumbnail);
266 | //console.log(srcThumbnail);
267 | const fileName = type + "_" + (new Date().getTime()) + ".jpg";
268 |
269 | const params = {
270 | ACL: props.bucketACL || 'public-read',
271 | Body: blob,
272 | Bucket: props.bucket,
273 | Key: fileName,
274 | ContentType: 'jpeg'
275 | };
276 |
277 | getMyBucket(props.bucket, props.awsRegion).putObject(params)
278 | .on('httpUploadProgress', (evt) => {
279 |
280 | const progressVal = Math.round((evt.loaded / evt.total) * 100);
281 | //console.logg('progress', progressVal);
282 | setProgressWrap(progressVal)
283 | if(progressVal === 100) {
284 | var clipDuration = 0;
285 | if(videoStartPosition != null && videoEndPosition != null) {
286 | clipDuration = videoDuration - (videoEndPosition - videoStartPosition);
287 | } else if(videoStartPosition == null && videoEndPosition != null) {
288 | clipDuration = videoEndPosition;
289 | } else if(videoStartPosition != null && videoEndPosition == null) {
290 | clipDuration = videoDuration - videoStartPosition;
291 | } else {
292 | clipDuration = videoDuration
293 | }
294 | //console.log(videoStartPosition, videoEndPosition, videoDuration);
295 | setFlowWrap(Config.FLOW_SUCESS);
296 |
297 |
298 | let processedVideoSize = parseFloat( (parseFloat(videoSize.replace("KB", "")) * parseInt(clipDuration)) / parseInt(videoDuration) ).toFixed(2) + "KB";
299 |
300 | let result = {
301 | result: true,
302 | url: url,
303 | thumbnail: props.bucket + "/" + fileName,
304 | processedVideoDuration: clipDuration,
305 | processedVideoSize: processedVideoSize,
306 | originalVideoName: videoName,
307 | originalVideoSize: videoSize,
308 | originalVideoWidth: videoWidth,
309 | originalVideoHeight: videoHeight,
310 | originalVideoDuration: videoDuration,
311 | };
312 | props.onResult(result)
313 | }
314 |
315 | })
316 | .send((err) => {
317 | if (err) {
318 | if(props.onResult != null) {
319 | props.onResult(
320 | {result: false, error: err}
321 | )
322 | setFlowWrap(Config.FLOW_ERROR);
323 | }
324 | }
325 | })
326 | }
327 |
328 | async function getJobStatus(jobId, s3Input) {
329 |
330 | const jobStatusPromise = new AWS.MediaConvert({apiVersion: '2017-08-29'}).getJob({Id: jobId}).promise();
331 | jobStatusPromise.then(
332 | function(data) {
333 | setJobStatus(data.Job?.Status);
334 | if(data.Job?.Status === Constants.JOB_STATUS_SUBMITTED) {
335 | checkJobStatus(jobId, s3Input);
336 | } else if(data.Job?.Status === Constants.JOB_STATUS_COMPLETE) {
337 | deleteOriginal(s3Input);
338 | if(props.onResult != null) {
339 | //console.log(data.Job);
340 | const url = s3Input.replace("s3://", "").split(".")[0] + data.Job?.Settings.OutputGroups[0].CustomName + "." + ext;
341 | uploadThumbnail(url);
342 |
343 | }
344 | setFlowWrap(Config.FLOW_SUCESS);
345 | }
346 | },
347 | function(err) {
348 | //console.logg("Error", err);
349 | }
350 | );
351 |
352 | }
353 |
354 | function checkJobStatus(jobId, s3Input) {
355 |
356 | //console.logg('checking status');
357 |
358 | setTimeout(() => {
359 |
360 | //console.logg('getting status', s3Input);
361 | getJobStatus(jobId, s3Input)
362 |
363 | }, 10000);
364 |
365 | }
366 |
367 | async function createMediaConvertJob(s3Input) {
368 |
369 | const jobTemplate = {
370 | "Role": "arn:aws:iam::181895849565:role/" + props.mediaConvertRole,
371 | "Settings": {
372 | "Inputs": [
373 | {
374 | "TimecodeSource": "ZEROBASED",
375 | "VideoSelector": {},
376 | "AudioSelectors": {
377 | "Audio Selector 1": {
378 | "DefaultSelection": "DEFAULT"
379 | }
380 | },
381 | "FileInput": "s3://" + s3Input,
382 | "InputClippings": [
383 | {
384 | "StartTimecode": (videoStartPosition > 0 && videoStartPosition != null) ? new Date(videoStartPosition * 1000).toISOString().substring(11, 19) + ":00" : "00:00:00:00",
385 | "EndTimecode": (videoEndPosition > 0 && videoEndPosition != null) ? new Date(videoEndPosition * 1000).toISOString().substring(11, 19) + ":00" : new Date(videoDuration * 1000).toISOString().substring(11, 19) + ":00",
386 | }
387 | ]
388 | }
389 | ],
390 | "OutputGroups": [
391 | {
392 | "Name": "File Group",
393 | "OutputGroupSettings": {
394 | "Type": "FILE_GROUP_SETTINGS",
395 | "FileGroupSettings": {
396 | "Destination": "s3://" + props.bucket + "/"
397 | }
398 | },
399 | "Outputs": [
400 | {
401 | "VideoDescription": {
402 | "CodecSettings": {
403 | "Codec": "H_264",
404 | "H264Settings": {
405 | "RateControlMode": "QVBR",
406 | "SceneChangeDetect": "TRANSITION_DETECTION",
407 | "MaxBitrate": 10000000,
408 | }
409 | }
410 | },
411 | "AudioDescriptions": [
412 | {
413 | "CodecSettings": {
414 | "Codec": "AAC",
415 | "AacSettings": {
416 | "Bitrate": 96000,
417 | "CodingMode": "CODING_MODE_2_0",
418 | "SampleRate": 48000
419 | }
420 | }
421 | }
422 | ],
423 | "ContainerSettings": {
424 | "Container": "MP4",
425 | "Mp4Settings": {}
426 | },
427 | "NameModifier": "output1"
428 | }
429 | ],
430 | "CustomName": "output1"
431 | }
432 | ],
433 | "TimecodeConfig": {
434 | "Source": "ZEROBASED"
435 | }
436 | }
437 | };
438 |
439 | //console.log(jobTemplate)
440 |
441 | const endpointPromise = new AWS.MediaConvert({apiVersion: '2017-08-29'}).createJob(jobTemplate).promise();
442 |
443 | // Handle promise's fulfilled/rejected status
444 | endpointPromise.then(
445 | function(data) {
446 | //console.logg("Job created! ", data.Job?.Id, Constants.JOB_STATUS_SUBMITTED);
447 | setJobStatus(Constants.JOB_STATUS_SUBMITTED);
448 | checkJobStatus(data.Job?.Id, s3Input);
449 | setFlowWrap(Config.FLOW_VIDEO_PROCESSING)
450 | },
451 | function(err) {
452 | //console.logg("Error", err);
453 | }
454 | );
455 | }
456 |
457 | const uploadFile = (ext) => {
458 |
459 | let blob = null;
460 |
461 | if(flow === Config.FLOW_CROP || flow === Config.FLOW_PREVIEW) {
462 | const canvasTemp = refCanvas.current;
463 | blob = dataURItoBlob(canvasTemp.toDataURL(fileType));
464 | } else if(flow === Config.FLOW_IMG_CHOSEN || flow === Config.FLOW_PDF_CHOSEN || flow == Config.FLOW_PDF_PREVIEW || flow == Config.FLOW_VIDEO_CHOSEN || flow == Config.FLOW_VIDEO_PREVIEW) {
465 | blob = dataURItoBlob(src);
466 | }
467 |
468 | const fileName = type + "_" + (new Date().getTime()) + "." + ext;
469 |
470 | const params = {
471 | ACL: props.bucketACL || 'public-read',
472 | Body: blob,
473 | Bucket: props.bucket,
474 | Key: fileName,
475 | ContentType: fileType
476 | };
477 |
478 | //console.log('upload file', fileName);
479 |
480 | getMyBucket(props.bucket, props.awsRegion).putObject(params)
481 | .on('httpUploadProgress', (evt) => {
482 |
483 | const progressVal = Math.round((evt.loaded / evt.total) * 100);
484 | //console.logg('progress', progressVal);
485 | setProgressWrap(progressVal)
486 |
487 | if(progressVal === 100) {
488 |
489 | if(type === Constants.TYPE_VIDEO) {
490 | if((videoStartPosition != null && videoStartPosition > 0)
491 | || (videoEndPosition != null && videoEndPosition > 0)) {
492 | setFlowWrap(Config.FLOW_VIDEO_PROCESSING)
493 | createMediaConvertJob(props.bucket + "/" + fileName)
494 | } else {
495 | uploadThumbnail(props.bucket + "/" + fileName);
496 | }
497 | clearInputs();
498 | } else {
499 | if(props.onResult != null) {
500 | props.onResult(
501 | {result: true, url: props.bucket + "/" + fileName}
502 | )
503 | }
504 | setFlowWrap(Config.FLOW_SUCESS);
505 | clearInputs();
506 | }
507 |
508 | }
509 |
510 | })
511 | .send((err) => {
512 | if (err) {
513 | if(props.onResult != null) {
514 | props.onResult(
515 | {result: false, error: err}
516 | )
517 | setFlowWrap(Config.FLOW_ERROR);
518 | }
519 | }
520 | })
521 | }
522 |
523 | function openDialog () {
524 |
525 | //console.logg('props', props.type);
526 |
527 | if(type === Constants.TYPE_IMAGE) {
528 |
529 | if(flow === Config.FLOW_INIT) {
530 | refInputImage.current.click();
531 | }
532 |
533 | } else if(type === Constants.TYPE_PDF) {
534 |
535 | if(flow === Config.FLOW_INIT) {
536 | refInputPdf.current.click();
537 | }
538 |
539 | } else {
540 |
541 | if(flow === Config.FLOW_INIT) {
542 | refInputVideo.current.click();
543 | }
544 |
545 | }
546 |
547 | }
548 |
549 | function onFileSelectionChanged(event) {
550 |
551 | let reader;
552 | if(type === Constants.TYPE_IMAGE) {
553 |
554 | const [file] = refInputImage.current.files
555 | //console.logg(file);
556 | if (file) {
557 | reader = new FileReader();
558 | reader.onload = function(){
559 |
560 | const fileName = file.name;
561 | const strArr = fileName.split(".");
562 | setExt(strArr[strArr.length - 1]);
563 | setFileType(file.type)
564 |
565 | const output = document.getElementById('output');
566 | setSrc(reader.result);
567 | setFlowWrap(Config.FLOW_IMG_CHOSEN);
568 | };
569 | reader.readAsDataURL(file);
570 | }
571 |
572 | } else if(type === Constants.TYPE_PDF) {
573 |
574 | const [file] = refInputPdf.current.files
575 | if(file) {
576 | //setPdf(file);
577 | setPdfName(file.name);
578 | setPdfSize((file.size / 1024) + 'KB')
579 | setFlowWrap(Config.FLOW_PDF_CHOSEN);
580 |
581 | reader = new FileReader();
582 | reader.onload = function(){
583 |
584 | //console.logg(file);
585 | const fileName = file.name;
586 | const strArr = fileName.split(".");
587 | setExt(strArr[strArr.length - 1]);
588 | setFileType(file.type)
589 | setSrc(reader.result);
590 | //console.logg(reader.result);
591 |
592 | };
593 | reader.readAsDataURL(file);
594 |
595 | }
596 |
597 | } else {
598 |
599 | const [file] = refInputVideo.current.files
600 | // console.log(file);
601 |
602 | if(file) {
603 | reader = new FileReader();
604 | reader.onload = function(){
605 |
606 | const fileName = file.name;
607 | const strArr = fileName.split(".");
608 | setVideoName(file.name);
609 | setVideoSize((file.size / 1024) + 'KB')
610 | setExt(strArr[strArr.length - 1]);
611 | setFileType(file.type)
612 | setSrc(reader.result);
613 | setFlowWrap(Config.FLOW_VIDEO_CHOSEN);
614 |
615 | };
616 | reader.readAsDataURL(file);
617 | }
618 |
619 | if(file) {
620 | const video = document.createElement('video');
621 | video.preload = 'metadata';
622 |
623 | video.onloadedmetadata = function() {
624 |
625 | const duration = video.duration;
626 | setVideoDuration(parseInt(duration));
627 | setVideoWidth(parseInt(video.videoWidth));
628 | setVideoHeight(parseInt(video.videoHeight));
629 | }
630 | video.src = URL.createObjectURL(file);
631 | video.load();
632 | }
633 |
634 | }
635 |
636 | }
637 |
638 | function showThumbnail(value) {
639 | if(value) {
640 | setSubFlowWrap(Config.FLOW_VIDEO_PREVIEW_THUMBNAIL_VIEW);
641 | } else {
642 | setSubFlowWrap(Config.FLOW_VIDEO_PREVIEW_INIT);
643 | }
644 | }
645 |
646 | function captureThumbnail() {
647 | const canvasScreenWidth = 100;
648 | const canvasScreenHeight = (canvasScreenWidth*videoHeight) / videoWidth;
649 |
650 | const ctxTemp = refCanvasThumbnail.current.getContext('2d');
651 |
652 | //console.log('screen Height', canvasScreenHeight);
653 |
654 | refCanvasThumbnail.current.style.height = canvasScreenHeight;
655 | refCanvasThumbnail.current.width = videoWidth;
656 | refCanvasThumbnail.current.height = videoHeight;
657 |
658 | ctxTemp.imageSmoothingEnabled = false;
659 | ctxTemp.drawImage(
660 | refInputVideoPreview.current,
661 | 0,
662 | 0,
663 | videoWidth,
664 | videoHeight);
665 |
666 | setSrcThumbnail(refCanvasThumbnail.current.toDataURL('image/jpeg'));
667 | }
668 |
669 | useEffect(() => {
670 |
671 | if(flow === Config.FLOW_VIDEO_PREVIEW && subFlow === Config.FLOW_VIDEO_PREVIEW_THUMBNAIL_VIEW) {
672 |
673 |
674 |
675 | }
676 |
677 | }, [subFlow])
678 |
679 | // Draws the video thumbnail on canvas
680 |
681 | useEffect(() => {
682 |
683 | if(flow === Config.FLOW_VIDEO_PREVIEW && videoWidth > 0 && videoHeight > 0) {
684 |
685 | refInputVideoPreview.current.addEventListener('play', (event) => {
686 |
687 | captureThumbnail();
688 |
689 | })
690 | }
691 |
692 | }, [flow, videoWidth, videoHeight])
693 |
694 |
695 | // Draws the cropped area
696 |
697 | useEffect(() => {
698 |
699 | if(flow === Config.FLOW_CROP) {
700 |
701 | const image = new Image();
702 | image.src = src;
703 | image.onload = function(){
704 |
705 | const imageW = image.width;
706 | const imageH = image.height;
707 |
708 | const canvasTemp = refCanvas.current;
709 | const ctxTemp = canvasTemp.getContext('2d');
710 |
711 | const sx1 = parseInt((cropInitX1*image.width)/100);
712 | const sy1 = parseInt((cropInitY1*image.height)/100);
713 |
714 | const sx2 = parseInt((cropInitX2*image.width)/100);
715 | const sy2 = parseInt((cropInitY2*image.height)/100);
716 |
717 | //console.logg('sx sy', sx1, sy1, sx2, sy2);
718 |
719 | const sw = sx2 - sx1;
720 | const sh = sy2 - sy1;
721 |
722 | canvasTemp.width = sw;
723 | canvasTemp.height = sh;
724 |
725 | //console.logg('sw sh', sw, sh, (sw/sh));
726 |
727 | const dx = 0;
728 | const dy = 0;
729 |
730 | const dw = canvasTemp.width;
731 | const dh = canvasTemp.height;
732 |
733 | //console.logg('dw dh', dw, dh, (dw/dh));
734 |
735 | ctxTemp.imageSmoothingEnabled = false;
736 | ctxTemp.drawImage(
737 | image,
738 | sx1,
739 | sy1,
740 | sw,
741 | sh,
742 | dx,
743 | dy,
744 | dw,
745 | dh);
746 |
747 | }
748 |
749 | }
750 |
751 | }, [flow, src, cropInitX1, cropInitX2, cropInitY1, cropInitY2])
752 |
753 | // Shows the drag point during cropping
754 |
755 | useEffect(() => {
756 |
757 | if(flow === Config.FLOW_CROP && moveX > 0 && moveY > 0) {
758 |
759 | const canvas = refCanvasOverlay.current;
760 | const ctx = canvas.getContext('2d');
761 |
762 | const containerW = refCanvasContainer.current.clientWidth;
763 | const containerH = refCanvasContainer.current.clientHeight;
764 |
765 | canvas.width = containerW;
766 | canvas.height = containerH;
767 |
768 | ctx.strokeStyle = '#ffffff';
769 | ctx.setLineDash([3, 5]);
770 | ctx.clearRect(0, 0, canvas.width, canvas.height);
771 |
772 | ctx.beginPath();
773 | ctx.moveTo(moveX, 0);
774 | ctx.lineTo(moveX, containerH);
775 | ctx.stroke();
776 |
777 | ctx.beginPath();
778 | ctx.moveTo(0, moveY);
779 | ctx.lineTo(containerW, moveY);
780 |
781 | ctx.stroke();
782 |
783 | } else {
784 |
785 | const canvas = refCanvasOverlay.current;
786 | if(canvas != null) {
787 |
788 | const ctx = canvas.getContext('2d');
789 |
790 | const containerW = refCanvasContainer.current.clientWidth;
791 | const containerH = refCanvasContainer.current.clientHeight;
792 |
793 | canvas.width = containerW;
794 | canvas.height = containerH;
795 |
796 | ctx.clearRect(0, 0, canvas.width, canvas.height);
797 |
798 | }
799 |
800 | }
801 |
802 | }, [moveX, moveY])
803 |
804 | // Moves the edges of the cropped area after drag
805 |
806 | useEffect(() => {
807 |
808 | //console.logg('useeffect', flow, teX, teY)
809 |
810 | if(flow === Config.FLOW_CROP && teX > 0 && teY > 0) {
811 | guessMovement();
812 | setTeX(0);
813 | setTeY(0);
814 | setTsX(0);
815 | setTsY(0);
816 | }
817 |
818 | }, [teX, teY])
819 |
820 | useEffect(() => {
821 | function handleResize() {
822 | setWindowDimensions(getWindowDimensions());
823 | }
824 |
825 | window.addEventListener('resize', handleResize);
826 | return () => window.removeEventListener('resize', handleResize);
827 | }, []);
828 |
829 | useEffect(() => {
830 | setType(props.type);
831 | }, [props.type])
832 |
833 | // Mouse Events
834 |
835 | function onMouseMove(event) {
836 |
837 | let y;
838 | let x;
839 | let rect;
840 | //console.logg('mouesmove', event.type);
841 |
842 | if(event.type === 'mousemove') {
843 |
844 | if(flow === Config.FLOW_CROP && tsX > 0 && tsY > 0) {
845 | rect = event.target.getBoundingClientRect();
846 | x = event.clientX - rect.left;
847 | y = event.clientY - rect.top;
848 | setMoveX(parseInt(x))
849 | setMoveY(parseInt(y))
850 | }
851 |
852 | } else {
853 |
854 | if(flow === Config.FLOW_CROP) {
855 | rect = event.target.getBoundingClientRect();
856 | x = event.changedTouches[0].clientX - rect.left;
857 | y = event.changedTouches[0].clientY - rect.top;
858 | setMoveX(parseInt(x))
859 | setMoveY(parseInt(y))
860 | }
861 |
862 | }
863 | }
864 |
865 | function onMouseDown(event) {
866 |
867 | let y;
868 | let x;
869 | let rect;
870 | //console.logg(event.type);
871 |
872 | if(event.type === 'mousedown') {
873 | if(flow === Config.FLOW_CROP) {
874 | rect = event.target.getBoundingClientRect();
875 | x = event.clientX - rect.left;
876 | y = event.clientY - rect.top;
877 |
878 |
879 | //console.logg(event.clientX, event.clientY);
880 | //console.logg(event.screenX, event.screenY);
881 | //console.logg(rect.left, rect.top);
882 |
883 |
884 | setTsX(parseInt(x))
885 | setTsY(parseInt(y))
886 | }
887 | } else {
888 | if(flow === Config.FLOW_CROP) {
889 | rect = event.target.getBoundingClientRect();
890 | x = event.touches[0].clientX - rect.left;
891 | y = event.touches[0].clientY - rect.top;
892 | setTsX(parseInt(x))
893 | setTsY(parseInt(y))
894 | }
895 | }
896 |
897 |
898 | }
899 |
900 | function onMouseUp(event) {
901 |
902 | let y;
903 | let x;
904 | let rect;
905 | //console.logg('mouseUp', event.type)
906 |
907 | if(event.type === 'mouseup') {
908 |
909 | if(flow === Config.FLOW_CROP) {
910 |
911 | rect = event.target.getBoundingClientRect();
912 | x = event.clientX - rect.left;
913 | y = event.clientY - rect.top;
914 |
915 | //console.logg(event.clientX, event.clientY);
916 | //console.logg(event.screenX, event.screenY);
917 | //console.logg(rect.left, rect.top);
918 |
919 |
920 | setTeX(parseInt(x))
921 | setTeY(parseInt(y))
922 | setMoveX(0)
923 | setMoveY(0)
924 | }
925 |
926 | } else {
927 |
928 | if(flow === Config.FLOW_CROP) {
929 | rect = event.target.getBoundingClientRect();
930 | x = event.changedTouches[0].clientX - rect.left;
931 | y = event.changedTouches[0].clientY - rect.top;
932 | setTeX(parseInt(x))
933 | setTeY(parseInt(y))
934 | setMoveX(0)
935 | setMoveY(0)
936 | }
937 |
938 | }
939 |
940 | }
941 |
942 | // Code to move the edges
943 |
944 | function moveLeft(edge, percent) {
945 | edge === 'L' ? setCropInitX1(cropInitX1 - percent > 0 ? cropInitX1 - percent : cropInitX1) : setCropInitX2(cropInitX2 - percent > Config.MAX_CROP ? cropInitX2 - percent : cropInitX2)
946 | }
947 |
948 | function moveRight(edge,percent) {
949 | edge === 'L' ? setCropInitX1(cropInitX1 + percent < Config.MAX_CROP ? cropInitX1 + percent : cropInitX1) : setCropInitX2(cropInitX2 + percent < 100 ? cropInitX2 + percent : cropInitX2)
950 | }
951 |
952 | function moveAbove(edge,percent) {
953 | edge === 'T' ? setCropInitY1(cropInitY1 - percent > 0 ? cropInitY1 - percent : cropInitY1) : setCropInitY2(cropInitY2 - percent > Config.MAX_CROP ? cropInitY2 - percent : cropInitY2)
954 | }
955 |
956 | function moveBelow(edge,percent) {
957 | edge === 'T' ? setCropInitY1(cropInitY1 + percent < (100 - Config.MAX_CROP) ? cropInitY1 + percent : cropInitY1) : setCropInitY2(cropInitY2 + percent < 100 ? cropInitY2 + percent : cropInitY2)
958 | }
959 |
960 |
961 | //Calculates the actual movement as a result of drag
962 |
963 | function guessMovement() {
964 |
965 | // Guess the direction
966 |
967 | let axis = "";
968 | let direction = "";
969 |
970 | const movementX = (teX - tsX);
971 | const movementY = (teY - tsY);
972 |
973 | const absMovementX = Math.abs(teX - tsX);
974 | const absMovementY = Math.abs(teY - tsY);
975 |
976 | if(absMovementX > absMovementY) {
977 | axis = 'H'
978 | if(movementX < 0) {
979 | direction = 'L'
980 | } else {
981 | direction = 'R';
982 | }
983 | } else {
984 | axis = 'V'
985 | if(movementY < 0) {
986 | direction = 'T'
987 | } else {
988 | direction = 'B';
989 | }
990 | }
991 | // Guess the part of view on which interaction started
992 |
993 | const canvasW = refCanvas.current.clientWidth;
994 | const canvasH = refCanvas.current.clientHeight;
995 |
996 | const containerW = refCanvasContainer.current.clientWidth;
997 | const containerH = refCanvasContainer.current.clientHeight;
998 |
999 | if(((Math.abs(tsX - (cropInitX1*containerW)/100) * 100)/canvasW) < Config.CROP_EDGE_SENSITIVITY && ((Math.abs(tsY - (cropInitY1*containerH)/100) * 100)/canvasH) < Config.CROP_EDGE_SENSITIVITY) {
1000 | //console.logg('TL', axis, direction);
1001 | if(axis === 'H' && direction === "L") moveLeft('L', (absMovementX*100)/containerW)
1002 | if(axis === 'H' && direction === "R") moveRight('L', (absMovementX*100)/containerW)
1003 | if(axis === 'V' && direction === "T") moveAbove('T', (absMovementY*100)/containerW)
1004 | if(axis === 'V' && direction === "B") moveBelow('T', (absMovementY*100)/containerW)
1005 | } else if(((Math.abs(tsX - (cropInitX1*containerW)/100) * 100)/canvasW) < Config.CROP_EDGE_SENSITIVITY && ((Math.abs(tsY - (cropInitY2*containerH)/100) * 100)/canvasH) < Config.CROP_EDGE_SENSITIVITY) {
1006 | //console.logg('BL', axis, direction);
1007 | if(axis === 'H' && direction === "L") moveLeft('L', (absMovementX*100)/containerW)
1008 | if(axis === 'H' && direction === "R") moveRight('L', (absMovementX*100)/containerW)
1009 | if(axis === 'V' && direction === "T") moveAbove('B', (absMovementY*100)/containerW)
1010 | if(axis === 'V' && direction === "B") moveBelow('B', (absMovementY*100)/containerW)
1011 | } else if(((Math.abs(tsX - (cropInitX2*containerW)/100) * 100)/canvasW) < Config.CROP_EDGE_SENSITIVITY && ((Math.abs(tsY - (cropInitY2*containerH)/100) * 100)/canvasH) < Config.CROP_EDGE_SENSITIVITY) {
1012 | //console.logg('BR', axis, direction);
1013 | if(axis === 'H' && direction === "L") moveLeft('R', (absMovementX*100)/containerW)
1014 | if(axis === 'H' && direction === "R") moveRight('R', (absMovementX*100)/containerW)
1015 | if(axis === 'V' && direction === "T") moveAbove('B', (absMovementY*100)/containerW)
1016 | if(axis === 'V' && direction === "B") moveBelow('B', (absMovementY*100)/containerW)
1017 | } else if(((Math.abs(tsX - (cropInitX2*containerW)/100) * 100)/canvasW) < Config.CROP_EDGE_SENSITIVITY && ((Math.abs(tsY - (cropInitY1*containerH)/100) * 100)/canvasH) < Config.CROP_EDGE_SENSITIVITY) {
1018 | //console.logg('TR', axis, direction);
1019 | if(axis === 'H' && direction === "L") moveLeft('R', (absMovementX*100)/containerW)
1020 | if(axis === 'H' && direction === "R") moveRight('R', (absMovementX*100)/containerW)
1021 | if(axis === 'V' && direction === "T") moveAbove('T', (absMovementY*100)/containerW)
1022 | if(axis === 'V' && direction === "B") moveBelow('T', (absMovementY*100)/containerW)
1023 | } else if(((Math.abs(tsX - (cropInitX1*containerW)/100) * 100)/canvasW) < Config.CROP_EDGE_SENSITIVITY) {
1024 | //console.logg('LE', axis, direction);
1025 | if(axis === 'H' && direction === "L") moveLeft('L', (absMovementX*100)/containerW)
1026 | if(axis === 'H' && direction === "R") moveRight('L', (absMovementX*100)/containerW)
1027 | } else if(((Math.abs(tsX - (cropInitX2*containerW)/100) * 100)/canvasW) < Config.CROP_EDGE_SENSITIVITY) {
1028 | //console.logg('RE', axis, direction);
1029 | if(axis === 'H' && direction === "L") moveLeft('R', (absMovementX*100)/containerW)
1030 | if(axis === 'H' && direction === "R") moveRight('R', (absMovementX*100)/containerW)
1031 | } else if(((Math.abs(tsY - (cropInitY1*containerH)/100) * 100)/canvasH) < Config.CROP_EDGE_SENSITIVITY) {
1032 | //console.logg('TE', axis, direction);
1033 | if(axis === 'V' && direction === "T") moveAbove('T', (absMovementY*100)/containerW)
1034 | if(axis === 'V' && direction === "B") moveBelow('T', (absMovementY*100)/containerW)
1035 | } else if(((Math.abs(tsY - (cropInitY2*containerH)/100) * 100)/canvasH) < Config.CROP_EDGE_SENSITIVITY) {
1036 | //console.logg('BE', axis, direction);
1037 | if(axis === 'V' && direction === "T") moveAbove('B', (absMovementY*100)/containerW)
1038 | if(axis === 'V' && direction === "B") moveBelow('B', (absMovementY*100)/containerW)
1039 | } else {
1040 | //console.logg('CE', axis, direction);
1041 | if(axis === 'H' && direction === "L") {
1042 | moveLeft('L', (absMovementX*100)/containerW)
1043 | moveLeft('R', (absMovementX*100)/containerW)
1044 | } else if (axis === 'H' && direction === "R") {
1045 | moveRight('L', (absMovementX*100)/containerW)
1046 | moveRight('R', (absMovementX*100)/containerW)
1047 | } else if(axis === 'V' && direction === "T") {
1048 | moveAbove('T', (absMovementY*100)/containerW)
1049 | moveAbove('B', (absMovementY*100)/containerW)
1050 | } else {
1051 | moveBelow('T', (absMovementY*100)/containerW)
1052 | moveBelow('B', (absMovementY*100)/containerW)
1053 | }
1054 | }
1055 |
1056 | }
1057 |
1058 | function clearInputs() {
1059 |
1060 | refInputVideo.current.value = '';
1061 | refInputImage.current.value = '';
1062 | refInputPdf.current.value = '';
1063 |
1064 | }
1065 |
1066 | return (
1067 |
1072 |
1073 |
1074 |
1079 | {
1080 | flow === Config.FLOW_INIT ? Constants.TITLE_FLOW_INIT : flow === Config.FLOW_IMG_CHOSEN ? Constants.TITLE_FLOW_IMAGE_CHOSEN : flow === Config.FLOW_CROP ? Constants.TITLE_FLOW_CROP : flow === Config.FLOW_PREVIEW ? Constants.TITLE_FLOW_PREVIEW : flow === Config.FLOW_UPLOAD ? Constants.TITLE_FLOW_UPLOAD : flow === Config.FLOW_SUCESS ? Constants.TITLE_FLOW_SUCCESS : flow === Config.FLOW_ERROR ? Constants.TITLE_FLOW_ERROR : flow === Config.FLOW_PDF_CHOSEN ? Constants.TITLE_FLOW_PDF_CHOSEN : flow === Config.FLOW_PDF_PREVIEW ? Constants.TITLE_FLOW_PDF_PREVIEW : flow === Config.FLOW_VIDEO_CHOSEN ? Constants.TITLE_FLOW_VIDEO_CHOSEN : flow === Config.FLOW_VIDEO_PREVIEW ? Constants.TITLE_FLOW_VIDEO_PREVIEW : flow === Config.FLOW_VIDEO_PROCESSING ? Constants.TITLE_FLOW_VIDEO_PROCESSING : ""
1081 | }
1082 | { flow === Config.FLOW_SUCESS && }
1083 | { flow === Config.FLOW_ERROR && }
1084 |
1085 |
1086 |
1087 |
1088 |
1089 |
1093 |
1094 | {
1095 | flow === Config.FLOW_INIT ? (type === 'image' ? 'Images ' + Constants.HINT_FLOW_INIT : type === 'video' ? 'Videos ' + Constants.HINT_FLOW_INIT : 'PDFs ' + Constants.HINT_FLOW_INIT) : flow === Config.FLOW_IMG_CHOSEN ? Constants.HINT_FLOW_IMAGE_CHOSEN : flow === Config.FLOW_CROP ? Constants.HINT_FLOW_CROP : flow === Config.FLOW_PREVIEW ? Constants.HINT_FLOW_PREVIEW : flow === Config.FLOW_UPLOAD ? Constants.HINT_FLOW_UPLOAD : flow === Config.FLOW_SUCESS ? Constants.HINT_FLOW_SUCCESS : flow === Config.FLOW_ERROR ? Constants.HINT_FLOW_ERROR : flow === Config.FLOW_PDF_CHOSEN ? Constants.HINT_FLOW_PDF_CHOSEN : flow === Config.FLOW_PDF_PREVIEW ? Constants.HINT_FLOW_PDF_PREVIEW : flow === Config.FLOW_VIDEO_CHOSEN ? Constants.HINT_FLOW_VIDEO_CHOSEN : flow === Config.FLOW_VIDEO_PREVIEW ? Constants.HINT_FLOW_VIDEO_PREVIEW : flow === Config.FLOW_VIDEO_PROCESSING ? Constants.HINT_FLOW_VIDEO_PROCESSING : ""
1096 | }
1097 |
1098 |
1099 |
1100 |
1101 | {flow === Config.FLOW_INIT &&
1102 |
1103 |
1104 |
1105 |
1106 |
1107 |
1108 |
1109 | }
1110 | {(flow === Config.FLOW_IMG_CHOSEN) &&
1111 |
1112 |
1113 |
1114 |
1115 | {event.stopPropagation(); onCancelClicked();}}/>
1116 |
1117 |
1118 | {event.stopPropagation(); onCropClicked();}} custom={{backgroundColor: props.theme != null ? props.theme.uploadToS3CancelBackgroundColor : defaultTheme.uploadToS3CancelBackgroundColor, color: props.theme != null ? props.theme.uploadToS3CancelColor : defaultTheme.uploadToS3CancelColor}} />
1119 |
1120 |
{event.stopPropagation(); onUploadClick()}}/>
1121 |
1122 |
1123 |
1124 | }
1125 | {(flow === Config.FLOW_PDF_CHOSEN) &&
1126 |
1127 |
1128 |
1129 |
1130 |
1131 | {
1132 | pdfName + ' ' + parseFloat(pdfSize).toFixed(2) + ' KB'
1133 | }
1134 |
1135 |
1136 |
1137 | }
1138 | {(flow === Config.FLOW_VIDEO_CHOSEN) &&
1139 |
1140 |
1141 |
1142 |
1143 |
1144 | {
1145 | videoName
1146 | }
1147 |
Duration
1148 | {
1149 | new Date(videoDuration * 1000).toISOString().substring(11, 19)
1150 | }
1151 |
Size
1152 | {
1153 | parseFloat(videoSize).toFixed(2) + ' KB'
1154 | }
1155 |
1156 |
1157 |
1158 | }
1159 | {(flow === Config.FLOW_PDF_CHOSEN) &&
1160 |
1161 |
1162 |
1163 |
1164 | {event.stopPropagation(); onCancelClicked();}} />
1165 |
1166 |
1167 | {event.stopPropagation(); onPreviewClicked();}} custom={{backgroundColor: props.theme != null ? props.theme.uploadToS3CancelBackgroundColor : defaultTheme.uploadToS3CancelBackgroundColor, color: props.theme != null ? props.theme.uploadToS3CancelColor : defaultTheme.uploadToS3CancelColor}} />
1168 |
1169 |
{event.stopPropagation(); onUploadClick()}}/>
1170 |
1171 |
1172 |
1173 | }
1174 | {(flow === Config.FLOW_VIDEO_CHOSEN) &&
1175 |
1176 |
1177 |
1178 |
1179 | {event.stopPropagation(); onCancelClicked();}}/>
1180 |
1181 |
{event.stopPropagation(); onPreviewClicked();}} custom={{backgroundColor: props.theme != null ? props.theme.uploadToS3UploadBackgroundColor : defaultTheme.uploadToS3UploadBackgroundColor, color: props.theme != null ? props.theme.uploadToS3UploadColor : defaultTheme.uploadToS3UploadColor}} />
1182 | {/* {event.stopPropagation(); onUploadClick()}} /> */}
1183 |
1184 |
1185 |
1186 | }
1187 | {(flow === Config.FLOW_CROP) &&
1188 |
1189 |
1190 |
1191 |
1192 | {event.stopPropagation(); onCancelClicked();}}/>
1193 |
1194 |
1195 | {event.stopPropagation(); onPreviewClicked();}} custom={{backgroundColor: props.theme != null ? props.theme.uploadToS3CancelBackgroundColor : defaultTheme.uploadToS3CancelBackgroundColor, color: props.theme != null ? props.theme.uploadToS3CancelColor : defaultTheme.uploadToS3CancelColor}} />
1196 |
1197 |
{event.stopPropagation(); onUploadClick()}}/>
1198 |
1199 |
1200 |
1201 | }
1202 |
1203 | {(flow === Config.FLOW_PREVIEW) &&
1204 |
1205 |
1206 |
1207 |
1208 | {event.stopPropagation(); onCancelClicked();}}/>
1209 |
1210 |
{event.stopPropagation(); onUploadClick()}}/>
1211 |
1212 |
1213 |
1214 | }
1215 |
1216 | {(flow === Config.FLOW_PDF_PREVIEW) &&
1217 |
1218 |
1219 |
1220 |
1221 | {event.stopPropagation(); onCancelClicked();}}/>
1222 |
1223 |
{event.stopPropagation(); onUploadClick()}}/>
1224 |
1225 |
1226 |
1227 | }
1228 |
1229 | {(flow === Config.FLOW_VIDEO_PREVIEW) &&
1230 |
1231 |
1232 |
1233 |
1234 | {event.stopPropagation(); onCancelClicked();}}/>
1235 |
1236 |
1237 | {(videoStartPosition != null && (videoStartPosition > 0 || videoEndPosition > 0)) && {event.stopPropagation(); onUploadClick();}} custom={{backgroundColor: 'black', color: 'white'}} />}
1238 | {(videoStartPosition == null || (videoStartPosition === 0 && videoEndPosition === 0)) && }
1239 |
1240 | {(videoStartPosition == null || (videoStartPosition === 0 && videoEndPosition === 0)) &&
{event.stopPropagation(); onUploadClick()}} custom={{backgroundColor: props.theme != null ? props.theme.uploadToS3UploadBackgroundColor : defaultTheme.uploadToS3UploadBackgroundColor, color: props.theme != null ? props.theme.uploadToS3UploadColor : defaultTheme.uploadToS3UploadColor}} />}
1241 |
1242 |
1243 |
1244 |
1245 | }
1246 |
1247 | {(flow === Config.FLOW_SUCESS && (props.showNewUpload == null || (props.showNewUpload != null && props.showNewUpload != false))) &&
1248 |
1249 |
1250 |
1251 | {event.stopPropagation(); onNewUploadClick()}}/>
1252 |
1253 |
1254 |
1255 | }
1256 |
1257 | {(flow === Config.FLOW_ERROR) &&
1258 |
1259 |
1260 |
1261 | {event.stopPropagation(); onNewUploadClick()}}/>
1262 |
1263 |
1264 |
1265 | }
1266 |
1267 | {(flow === Config.FLOW_PDF_PREVIEW) &&
1268 |
1269 |
1270 |
1271 |
1272 |
1273 | }
1274 |
1275 | {(flow === Config.FLOW_VIDEO_PREVIEW) &&
1276 |
1277 |
1278 |
1279 |
1280 |
1283 |
1284 |
1285 | Thumbnail
1286 |
1287 |
1288 |
1289 |
1290 |
1291 |
1292 |
1293 |
1294 |
1295 |
1296 |
1299 |
1300 |
1301 | Preview & Clip Video
1302 |
1303 |
1304 |
1307 |
1313 |
1314 |
1323 |
1335 |
1336 |
1348 |
1349 |
1361 |
1362 |
1374 |
1375 |
1376 |
1377 |
1378 |
1379 |
1380 |
1381 |
1382 |
1383 |
1384 |
1385 |
1386 |
1387 |
1388 |
1389 |
1390 |
1391 |
1392 |
1393 |
1394 |
1395 | {(videoStartPosition != null && videoStartPosition > 0) &&
1396 |
1397 | {new Date(videoStartPosition * 1000).toISOString().substring(11, 19)}
1398 |
1399 |
1400 |
1401 | }
1402 |
1403 |
1404 |
1405 |
1406 | {(videoEndPosition != null && videoEndPosition > 0) &&
1407 |
1408 |
1409 |
1410 | {new Date(videoEndPosition * 1000).toISOString().substring(11, 19)}
1411 |
1412 | }
1413 |
1414 |
1415 |
1416 |
1417 |
1418 |
1419 |
1420 |
1421 |
1422 |
1423 |
1424 |
1425 |
1426 |
1427 |
1428 |
1429 |
1430 |
1431 |
1432 |
1433 |
1434 |
1435 |
1436 |
1437 |
1438 | }
1439 |
1440 | {(flow === Config.FLOW_VIDEO_PREVIEW && subFlow === Config.FLOW_VIDEO_PREVIEW_THUMBNAIL_VIEW) &&
1441 |
1449 |
1450 |
1455 |
1456 |

1460 |
1461 |
1473 |
1474 |
1475 |
1476 | }
1477 |
1478 | {(flow === Config.FLOW_IMG_CHOSEN || flow === Config.FLOW_CROP || flow === Config.FLOW_PREVIEW) &&
1479 |
1480 |
1481 | {onMouseMove(event)}}
1484 | onTouchStart={(event) => {onMouseDown(event)}}
1485 | onTouchEnd={(event) => {onMouseUp(event)}}
1486 | // onDrag={(event) => {onMouseMove(event)}}
1487 | // onDragStart={(event) => {onMouseDown(event)}}
1488 | // onDragEnd={(event) => {onMouseUp(event)}}
1489 | onMouseMove={(event) => {onMouseMove(event)}}
1490 | onMouseDown={(event) => {onMouseDown(event)}}
1491 | onMouseUp={(event) => {onMouseUp(event)}}
1492 | className='d-flex text-small justify-content-start mt-3 w-100'
1493 | style={{position: 'relative'}}>
1494 |
1495 |

1502 |
1503 |
1504 | {flow === Config.FLOW_CROP &&
1516 |
1517 |
}
1518 |
1519 | {(flow === Config.FLOW_CROP || flow === Config.FLOW_PREVIEW) &&
1540 |
1541 |
}
1542 |
1543 | {flow === Config.FLOW_CROP &&
1554 |
1555 |
}
1556 |
1557 |
1558 |
1559 | }
1560 |
1561 |
1562 |
1563 |
1564 |
1565 |
1566 |
1567 |
1568 |
1569 |
1570 |
1571 |
1572 |
1573 |
1574 |
1575 |
1576 |
1577 |
1578 |
1579 |
1580 |
1581 |
1582 |
1583 |
1584 |
1585 |
1586 |
1587 |
1588 |
1589 |
1590 | )
1591 | }
1592 |
--------------------------------------------------------------------------------