├── .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 | [![NPM](https://img.shields.io/npm/v/react-upload-to-s3.svg)](https://www.npmjs.com/package/react-upload-to-s3) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](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 | 21 | 22 | 23 | ### Upload a video to S3 (with thumbnail generator & easy clip) 24 | 25 | 32 | 33 | 34 | ### Upload a pdf to S3 35 | 36 | 41 | 42 | 43 | ## Demo 44 | 45 | [![Demo](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](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 | [![Demo](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](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 | --------------------------------------------------------------------------------