diff --git a/package.json b/package.json index d45eb12fe..1f44b48b4 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "ramda": "^0.27.1", "react": "^17.0.2", "react-autosuggest": "^9.4.2", + "react-cropper": "^2.1.8", "react-dnd": "^11.1.3", "react-dnd-touch-backend": "^11.1.3", "react-dom": "^17.0.2", diff --git a/src/components/Board/Board.container.js b/src/components/Board/Board.container.js index 9b27c71f3..83abe635c 100644 --- a/src/components/Board/Board.container.js +++ b/src/components/Board/Board.container.js @@ -1615,6 +1615,7 @@ export class BoardContainer extends Component { onEditSubmit={this.handleEditTileEditorSubmit} onAddSubmit={this.handleAddTileEditorSubmit} boards={this.props.boards} + userData={this.props.userData} /> ); diff --git a/src/components/Board/ImageEditor/ImageEditor.component.js b/src/components/Board/ImageEditor/ImageEditor.component.js new file mode 100644 index 000000000..7756d3255 --- /dev/null +++ b/src/components/Board/ImageEditor/ImageEditor.component.js @@ -0,0 +1,198 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import Dialog from '@material-ui/core/Dialog'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import DialogActions from '@material-ui/core/DialogActions'; +import IconButton from '../../UI/IconButton'; +import CloseIcon from '@material-ui/icons/Close'; +import withMobileDialog from '@material-ui/core/withMobileDialog'; +import messages from './ImageEditor.messages'; +import RotateRightIcon from '@material-ui/icons/RotateRight'; +import DoneIcon from '@material-ui/icons/Done'; +import CropIcon from '@material-ui/icons/Crop'; +import BlockIcon from '@material-ui/icons/Block'; +import ZoomInIcon from '@material-ui/icons/ZoomIn'; +import ZoomOutIcon from '@material-ui/icons/ZoomOut'; + +import './ImageEditor.css'; +import Cropper from 'react-cropper'; +import 'cropperjs/dist/cropper.css'; + +class ImageEditor extends PureComponent { + static defaultProps = { + open: false + }; + static propTypes = { + open: PropTypes.bool, + onImageEditorClose: PropTypes.func, + onImageEditorDone: PropTypes.func, + image: PropTypes.string, + intl: PropTypes.object.isRequired + }; + + constructor(props) { + super(props); + const setImageSize = () => { + if (window.innerWidth < 576) { + return { width: 248, height: 182 }; + } else { + return { width: 492, height: 369 }; + } + }; + this.state = { + isCropActive: false, + imgCropped: null, + style: setImageSize() + }; + } + + handleOnClickCrop = () => { + this.setState({ isCropActive: true }); + this.state.cropper.setDragMode('crop'); + this.state.cropper.crop(); + }; + handleOnClickDoneCrop = () => { + const { cropper } = this.state; + this.setState({ + isCropActive: false, + imgCropped: cropper.getCroppedCanvas().toDataURL() + }); + this.state.cropper.setDragMode('move'); + }; + handleOnClickClose = () => { + this.setState({ isCropActive: false, imgCropped: null }); + this.state.cropper.destroy(); + this.props.onImageEditorClose(); + }; + + handleOnClickDone = async () => { + const { cropper } = this.state; + cropper.setDragMode('move'); + this.setState({ imgCropped: null }); + this.props.onImageEditorClose(); + cropper + .getCroppedCanvas({ + maxWidth: 200, + maxHeight: 200, + fillColor: '#fff', + imageSmoothingEnabled: true, + imageSmoothingQuality: 'high' + }) + .toBlob( + blob => { + this.props.onImageEditorDone(blob); + }, + 'image/png', + 1 + ); + cropper.destroy(); + }; + handleOnClickCancelCrop = () => { + this.setState({ isCropActive: false }); + this.state.cropper.clear(); + this.state.cropper.setDragMode('move'); + }; + + render() { + const { intl, open, onImageEditorClose, image } = this.props; + const srcImage = this.state.imgCropped ? this.state.imgCropped : image; + return ( + + + +
+ +
+
+ + { + this.setState({ cropper: instance }); + }} + /> +
+ { + this.state.cropper.rotate(90); + }} + > + + + {this.state.isCropActive ? ( + + + + + + + + + ) : ( + + + + )} + this.state.cropper.zoom(0.1)} + > + + + this.state.cropper.zoom(-0.1)} + > + + +
+
+ + + + + + + + + +
+
+ ); + } +} + +export default withMobileDialog()(ImageEditor); diff --git a/src/components/Board/ImageEditor/ImageEditor.css b/src/components/Board/ImageEditor/ImageEditor.css new file mode 100644 index 000000000..ff3f3ca9f --- /dev/null +++ b/src/components/Board/ImageEditor/ImageEditor.css @@ -0,0 +1,18 @@ +.ImageEditor__container { + display: flex; + justify-content: space-between; + align-items: center; +} + +.ImageEditor__actionBar { + display: flex; + justify-content: center; + margin-bottom: 2px; +} + +.ImageEditor__title { + border-bottom: 1px solid #ccc; + margin-bottom: 18px; + padding: 20px 24px 18px; + position: relative; +} diff --git a/src/components/Board/ImageEditor/ImageEditor.messages.js b/src/components/Board/ImageEditor/ImageEditor.messages.js new file mode 100644 index 000000000..b14eb693d --- /dev/null +++ b/src/components/Board/ImageEditor/ImageEditor.messages.js @@ -0,0 +1,36 @@ +import { defineMessages } from 'react-intl'; + +export default defineMessages({ + title: { + id: 'cboard.components.Board.ImageEditor.title', + defaultMessage: 'Image editor' + }, + rotateRight: { + id: 'cboard.components.Board.ImageEditor.rotateRight', + defaultMessage: 'Rotate right' + }, + cropImage: { + id: 'cboard.components.Board.ImageEditor.cropImage', + defaultMessage: 'Crop image' + }, + close: { + id: 'cboard.components.Board.ImageEditor.close', + defaultMessage: 'Close' + }, + done: { + id: 'cboard.components.Board.ImageEditor.done', + defaultMessage: 'Done' + }, + cancelCrop: { + id: 'cboard.components.Board.ImageEditor.cancelCrop', + defaultMessage: 'Cancel crop' + }, + zoomIn: { + id: 'cboard.components.Board.ImageEditor.zoomIn', + defaultMessage: 'Zoom in' + }, + zoomOut: { + id: 'cboard.components.Board.ImageEditor.zoomOut', + defaultMessage: 'Zoom out' + } +}); diff --git a/src/components/Board/ImageEditor/index.js b/src/components/Board/ImageEditor/index.js new file mode 100644 index 000000000..8f391128a --- /dev/null +++ b/src/components/Board/ImageEditor/index.js @@ -0,0 +1 @@ +export { default } from './ImageEditor.component'; diff --git a/src/components/Board/SymbolSearch/SymbolSearch.component.js b/src/components/Board/SymbolSearch/SymbolSearch.component.js index 95ae47372..040900f30 100644 --- a/src/components/Board/SymbolSearch/SymbolSearch.component.js +++ b/src/components/Board/SymbolSearch/SymbolSearch.component.js @@ -259,8 +259,7 @@ export class SymbolSearch extends PureComponent { image: suggestion.src, label: suggestion.translatedId, labelKey: undefined - }); - onClose(); + }).then(() => onClose()); }; handleChange = (event, { newValue }) => { diff --git a/src/components/Board/TileEditor/TileEditor.component.js b/src/components/Board/TileEditor/TileEditor.component.js index 9b80cdec0..9c80bf343 100644 --- a/src/components/Board/TileEditor/TileEditor.component.js +++ b/src/components/Board/TileEditor/TileEditor.component.js @@ -30,6 +30,11 @@ import IconButton from '../../UI/IconButton'; import ColorSelect from '../../UI/ColorSelect'; import VoiceRecorder from '../../VoiceRecorder'; import './TileEditor.css'; +import EditIcon from '@material-ui/icons/Edit'; +import ImageEditor from '../ImageEditor'; + +import API from '../../../api'; +import { isAndroid, writeCvaFile } from '../../../cordova-util'; export class TileEditor extends Component { static propTypes = { @@ -57,11 +62,13 @@ export class TileEditor extends Component { * Callback fired when submitting a new board tile */ onAddSubmit: PropTypes.func.isRequired, - boards: PropTypes.array + boards: PropTypes.array, + userData: PropTypes.object }; static defaultProps = { - editingTiles: [] + editingTiles: [], + openImageEditor: false }; constructor(props) { @@ -91,7 +98,16 @@ export class TileEditor extends Component { isSymbolSearchOpen: false, selectedBackgroundColor: '', tile: this.defaultTile, - linkedBoard: '' + linkedBoard: '', + imageUploadedData: [], + isEditImageBtnActive: false + }; + + this.defaultimageUploadedData = { + isUploaded: false, + fileName: '', + blobHQ: null, + blob: null }; } @@ -135,51 +151,167 @@ export class TileEditor extends Component { } } - handleSubmit = () => { + handleSubmit = async () => { const { onEditSubmit, onAddSubmit } = this.props; - - this.setState({ - activeStep: 0, - selectedBackgroundColor: '', - tile: this.defaultTile - }); - if (this.editingTile()) { - onEditSubmit(this.state.editingTiles); + const { imageUploadedData } = this.state; + if (imageUploadedData.length) { + let tilesToAdd = JSON.parse(JSON.stringify(this.state.editingTiles)); + await Promise.all( + imageUploadedData.map(async (obj, index) => { + if (obj.isUploaded) { + tilesToAdd[index].image = await this.updateTileImgURL( + obj.blob, + obj.fileName + ); + } + }) + ); + onEditSubmit(tilesToAdd); + } else { + onEditSubmit(this.state.editingTiles); + } } else { const tileToAdd = this.state.tile; - const selectedBackgroundColor = this.state.selectedBackgroundColor; + const imageUploadedData = this.state.imageUploadedData[ + this.state.activeStep + ]; + if (imageUploadedData && imageUploadedData.isUploaded) { + tileToAdd.image = await this.updateTileImgURL( + imageUploadedData.blob, + imageUploadedData.fileName + ); + } + const selectedBackgroundColor = this.state.selectedBackgroundColor; if (selectedBackgroundColor) { tileToAdd.backgroundColor = selectedBackgroundColor; } onAddSubmit(tileToAdd); } + + this.setState({ + activeStep: 0, + selectedBackgroundColor: '', + tile: this.defaultTile, + imageUploadedData: [], + isEditImageBtnActive: false + }); + }; + + updateTileImgURL = async (blob, fileName) => { + const { userData } = this.props; + const user = userData.email ? userData : null; + if (user) { + // this.setState({ + // loading: true + // }); + try { + const imageUrl = await API.uploadFile(blob, fileName); + // console.log('imagen guardada en servidor', imageUrl); + return imageUrl; + } catch (error) { + //console.log('imagen no guardad en servidor'); + return await this.blobToBase64(blob); + } + // } finally { + // this.setState({ + // loading: false + // }); + } else { + if (isAndroid()) { + const filePath = '/Android/data/com.unicef.cboard/files/' + fileName; + const fEntry = await writeCvaFile(filePath, blob); + return fEntry.nativeURL; + } else { + return await this.blobToBase64(blob); + } + } + }; + + blobToBase64 = async blob => { + return new Promise(resolve => { + const reader = new FileReader(); + reader.onload = () => { + resolve(reader.result); + }; + reader.readAsDataURL(blob); + }); }; handleCancel = () => { const { onClose } = this.props; - this.setState({ activeStep: 0, selectedBackgroundColor: '', - tile: this.defaultTile + tile: this.defaultTile, + imageUploadedData: [], + isEditImageBtnActive: false }); onClose(); }; - handleInputImageChange = image => { + createimageUploadedDataArray() { + if (this.editingTile()) { + let imageUploadedDataArray = new Array(this.state.editingTiles.length); + imageUploadedDataArray.fill(this.defaultimageUploadedData); + this.setState({ imageUploadedData: imageUploadedDataArray }); + } else { + this.setState({ + imageUploadedData: new Array(this.defaultimageUploadedData) + }); + } + } + + handleInputImageChange = (blob, fileName, blobHQ) => { + if (!this.state.imageUploadedData.length) { + this.createimageUploadedDataArray(); + } + this.setimageUploadedData(true, fileName, blobHQ, blob); + this.setState({ isEditImageBtnActive: true }); + const image = URL.createObjectURL(blob); this.updateTileProperty('image', image); }; + setimageUploadedData = (isUploaded, fileName, blobHQ = null, blob = null) => { + const { activeStep } = this.state; + let imageUploadedData = this.state.imageUploadedData.map((item, indx) => { + if (indx === activeStep) { + return { + ...item, + isUploaded: isUploaded, + fileName: fileName, + blobHQ: blobHQ, + blob: blob + }; + } else { + return item; + } + }); + this.setState({ imageUploadedData: imageUploadedData }); + }; + handleSymbolSearchChange = ({ image, labelKey, label }) => { - this.updateTileProperty('labelKey', labelKey); - this.updateTileProperty('label', label); - this.updateTileProperty('image', image); + return new Promise(resolve => { + this.updateTileProperty('labelKey', labelKey); + this.updateTileProperty('label', label); + this.updateTileProperty('image', image); + if (this.state.imageUploadedData.length) { + this.setimageUploadedData(false, ''); + } + resolve(); + }); }; handleSymbolSearchClose = event => { + const { imageUploadedData } = this.state; this.setState({ isSymbolSearchOpen: false }); + if ( + imageUploadedData.length && + imageUploadedData[this.state.activeStep].isUploaded + ) { + this.setState({ isEditImageBtnActive: true }); + } }; handleLabelChange = event => { @@ -218,15 +350,18 @@ export class TileEditor extends Component { handleBack = event => { this.setState({ activeStep: this.state.activeStep - 1 }); this.setState({ selectedBackgroundColor: '', linkedBoard: '' }); + this.setState({ isEditImageBtnActive: false }); }; - handleNext = event => { + handleNext = async event => { this.setState({ activeStep: this.state.activeStep + 1 }); this.setState({ selectedBackgroundColor: '', linkedBoard: '' }); + this.setState({ isEditImageBtnActive: false }); }; handleSearchClick = event => { this.setState({ isSymbolSearchOpen: true }); + this.setState({ isEditImageBtnActive: false }); }; handleColorChange = event => { @@ -262,9 +397,24 @@ export class TileEditor extends Component { } }; + handleOnClickImageEditor = () => { + this.setState({ openImageEditor: true }); + }; + onImageEditorClose = () => { + this.setState({ openImageEditor: false }); + }; + onImageEditorDone = blob => { + this.setState(prevState => { + const newArray = [...prevState.imageUploadedData]; + newArray[this.state.activeStep].blob = blob; + return { imageUploadedData: newArray }; + }); + const image = URL.createObjectURL(blob); + this.updateTileProperty('image', image); + }; + render() { const { open, intl, boards } = this.props; - const currentLabel = this.currentTileProp('labelKey') ? intl.formatMessage({ id: this.currentTileProp('labelKey') }) : this.currentTileProp('label'); @@ -343,6 +493,29 @@ export class TileEditor extends Component { + {this.state.isEditImageBtnActive && ( + + + + + )}