diff --git a/web/cashtab/package-lock.json b/web/cashtab/package-lock.json
--- a/web/cashtab/package-lock.json
+++ b/web/cashtab/package-lock.json
@@ -5,6 +5,7 @@
"requires": true,
"packages": {
"": {
+ "name": "cashtab",
"version": "1.0.0",
"dependencies": {
"@ant-design/icons": "^4.3.0",
@@ -27,6 +28,7 @@
"react-dev-utils": "^11.0.4",
"react-device-detect": "^1.15.0",
"react-dom": "^17.0.1",
+ "react-easy-crop": "^3.5.3",
"react-ga": "^3.3.0",
"react-image": "^4.0.3",
"react-router": "^5.2.0",
@@ -16391,6 +16393,11 @@
"node": ">=6"
}
},
+ "node_modules/normalize-wheel": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz",
+ "integrity": "sha1-rsiGr/2wRQcNhWRH32Ls+GFG7EU="
+ },
"node_modules/npm-run-path": {
"version": "4.0.1",
"dev": true,
@@ -22024,6 +22031,24 @@
"react": "17.0.1"
}
},
+ "node_modules/react-easy-crop": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-3.5.3.tgz",
+ "integrity": "sha512-ApTbh+lzKAvKqYW81ihd5J6ZTNN3vPDwi6ncFuUrHPI4bko2DlYOESkRm+0NYoW0H8YLaD7bxox+Z3EvIzAbUA==",
+ "dependencies": {
+ "normalize-wheel": "^1.0.1",
+ "tslib": "2.0.1"
+ },
+ "peerDependencies": {
+ "react": ">=16.4.0",
+ "react-dom": ">=16.4.0"
+ }
+ },
+ "node_modules/react-easy-crop/node_modules/tslib": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
+ "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
+ },
"node_modules/react-error-overlay": {
"version": "6.0.9",
"license": "MIT"
@@ -39784,6 +39809,11 @@
"version": "3.3.0",
"dev": true
},
+ "normalize-wheel": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz",
+ "integrity": "sha1-rsiGr/2wRQcNhWRH32Ls+GFG7EU="
+ },
"npm-run-path": {
"version": "4.0.1",
"dev": true,
@@ -43390,6 +43420,22 @@
"scheduler": "^0.20.1"
}
},
+ "react-easy-crop": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-3.5.3.tgz",
+ "integrity": "sha512-ApTbh+lzKAvKqYW81ihd5J6ZTNN3vPDwi6ncFuUrHPI4bko2DlYOESkRm+0NYoW0H8YLaD7bxox+Z3EvIzAbUA==",
+ "requires": {
+ "normalize-wheel": "^1.0.1",
+ "tslib": "2.0.1"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
+ "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
+ }
+ }
+ },
"react-error-overlay": {
"version": "6.0.9"
},
diff --git a/web/cashtab/package.json b/web/cashtab/package.json
--- a/web/cashtab/package.json
+++ b/web/cashtab/package.json
@@ -24,6 +24,7 @@
"react-dev-utils": "^11.0.4",
"react-device-detect": "^1.15.0",
"react-dom": "^17.0.1",
+ "react-easy-crop": "^3.5.3",
"react-ga": "^3.3.0",
"react-image": "^4.0.3",
"react-router": "^5.2.0",
diff --git a/web/cashtab/src/components/App.js b/web/cashtab/src/components/App.js
--- a/web/cashtab/src/components/App.js
+++ b/web/cashtab/src/components/App.js
@@ -36,14 +36,14 @@
import { currency } from './Common/Ticker';
const GlobalStyle = createGlobalStyle`
- .ant-modal-wrap > div > div.ant-modal-content > div > div > div.ant-modal-confirm-btns > button, .ant-modal > button, .ant-modal-confirm-btns > button, .ant-modal-footer > button {
+ .ant-modal-wrap > div > div.ant-modal-content > div > div > div.ant-modal-confirm-btns > button, .ant-modal > button, .ant-modal-confirm-btns > button, .ant-modal-footer > button, #cropControlsConfirm {
border-radius: 8px;
background-color: ${props => props.theme.modals.buttons.background};
color: ${props => props.theme.wallet.text.secondary};
font-weight: bold;
}
- .ant-modal-wrap > div > div.ant-modal-content > div > div > div.ant-modal-confirm-btns > button:hover,.ant-modal-confirm-btns > button:hover, .ant-modal-footer > button:hover {
+ .ant-modal-wrap > div > div.ant-modal-content > div > div > div.ant-modal-confirm-btns > button:hover,.ant-modal-confirm-btns > button:hover, .ant-modal-footer > button:hover, #cropControlsConfirm:hover {
color: ${props => props.theme.primary};
transition: color 0.3s;
background-color: ${props => props.theme.modals.buttons.background};
@@ -61,15 +61,22 @@
color: ${props => props.theme.contrast} !important;
background-color: ${props => props.theme.primary} !important;
}
- #addrSwitch {
+ #addrSwitch, #cropSwitch {
.ant-switch-checked {
background-color: white !important;
}
}
- #addrSwitch.ant-switch-checked {
+ #addrSwitch.ant-switch-checked, #cropSwitch.ant-switch-checked {
background-image: ${props =>
props.theme.buttons.primary.backgroundImage} !important;
}
+
+ .ant-slider-rail {
+ background-color: ${props => props.theme.forms.border} !important;
+ }
+ .ant-slider-track {
+ background-color: ${props => props.theme.primary} !important;
+ }
`;
const CustomApp = styled.div`
diff --git a/web/cashtab/src/components/Common/CropControlModal.js b/web/cashtab/src/components/Common/CropControlModal.js
new file mode 100644
--- /dev/null
+++ b/web/cashtab/src/components/Common/CropControlModal.js
@@ -0,0 +1,59 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+import { Card, Modal } from 'antd';
+
+const CropModal = styled(Modal)`
+ .ant-modal-close-x {
+ font-size: 2px;
+ }
+`;
+
+export const CropperContainer = styled.div`
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 175px;
+`;
+export const ControlsContainer = styled.div`
+ position: absolute;
+ padding: 12px;
+ bottom: 0;
+ left: 50%;
+ width: 50%;
+ transform: translateX(-50%);
+ height: 175px;
+ display: block;
+ align-items: center;
+`;
+
+export const CropControlModal = ({
+ expand,
+ renderExpanded = () => null,
+ onClose,
+ style,
+ ...otherProps
+}) => {
+ return (
+
+ Click, or drag file to this + area to upload +
++ Only jpg or png accepted +
+ > + )} +You need at least @@ -83,14 +83,14 @@ exports[`Wallet with BCH balances and tokens 1`] = ` Array [
You need at least @@ -163,14 +163,14 @@ exports[`Wallet with BCH balances and tokens and state field 1`] = ` Array [
You need at least @@ -341,14 +329,14 @@ exports[`Without wallet defined 1`] = ` Array [
You need at least diff --git a/web/cashtab/src/utils/icons/cropImage.js b/web/cashtab/src/utils/icons/cropImage.js new file mode 100644 --- /dev/null +++ b/web/cashtab/src/utils/icons/cropImage.js @@ -0,0 +1,85 @@ +const createImage = url => + new Promise((resolve, reject) => { + const image = new Image(); + image.addEventListener('load', () => resolve(image)); + image.addEventListener('error', error => reject(error)); + image.setAttribute('crossOrigin', 'anonymous'); + image.src = url; + }); + +function getRadianAngle(degreeValue) { + return (degreeValue * Math.PI) / 180; +} + +export default async function getCroppedImg( + imageSrc, + pixelCrop, + rotation = 0, + fileName, +) { + const image = await createImage(imageSrc); + console.log('image :', image); + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + const maxSize = Math.max(image.width, image.height); + const safeArea = 2 * ((maxSize / 2) * Math.sqrt(2)); + + canvas.width = safeArea; + canvas.height = safeArea; + + ctx.translate(safeArea / 2, safeArea / 2); + ctx.rotate(getRadianAngle(rotation)); + ctx.translate(-safeArea / 2, -safeArea / 2); + + ctx.drawImage( + image, + safeArea / 2 - image.width * 0.5, + safeArea / 2 - image.height * 0.5, + ); + const data = ctx.getImageData(0, 0, safeArea, safeArea); + + canvas.width = pixelCrop.width; + canvas.height = pixelCrop.height; + + ctx.putImageData( + data, + 0 - safeArea / 2 + image.width * 0.5 - pixelCrop.x, + 0 - safeArea / 2 + image.height * 0.5 - pixelCrop.y, + ); + + if (!HTMLCanvasElement.prototype.toBlob) { + Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { + value: function (callback, type, quality) { + var dataURL = this.toDataURL(type, quality).split(',')[1]; + setTimeout(function () { + var binStr = atob(dataURL), + len = binStr.length, + arr = new Uint8Array(len); + for (var i = 0; i < len; i++) { + arr[i] = binStr.charCodeAt(i); + } + callback(new Blob([arr], { type: type || 'image/png' })); + }); + }, + }); + } + return new Promise(resolve => { + ctx.canvas.toBlob( + blob => { + const file = new File([blob], fileName, { + type: 'image/png', + }); + const resultReader = new FileReader(); + + resultReader.readAsDataURL(file); + + resultReader.addEventListener('load', () => + resolve({ file, url: resultReader.result }), + ); + }, + 'image/png', + 1, + ); + }); +} diff --git a/web/cashtab/src/utils/icons/resizeImage.js b/web/cashtab/src/utils/icons/resizeImage.js new file mode 100644 --- /dev/null +++ b/web/cashtab/src/utils/icons/resizeImage.js @@ -0,0 +1,57 @@ +const createImage = url => + new Promise((resolve, reject) => { + const image = new Image(); + image.addEventListener('load', () => resolve(image)); + image.addEventListener('error', error => reject(error)); + image.setAttribute('crossOrigin', 'anonymous'); + image.src = url; + }); + +export default async function getResizedImg(imageSrc, callback, fileName) { + const image = await createImage(imageSrc); + + const width = 512; + const height = 512; + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + + ctx.drawImage(image, 0, 0, width, height); + if (!HTMLCanvasElement.prototype.toBlob) { + Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { + value: function (callback, type, quality) { + var dataURL = this.toDataURL(type, quality).split(',')[1]; + setTimeout(function () { + var binStr = atob(dataURL), + len = binStr.length, + arr = new Uint8Array(len); + for (var i = 0; i < len; i++) { + arr[i] = binStr.charCodeAt(i); + } + callback(new Blob([arr], { type: type || 'image/png' })); + }); + }, + }); + } + + return new Promise(resolve => { + ctx.canvas.toBlob( + blob => { + const file = new File([blob], fileName, { + type: 'image/png', + }); + const resultReader = new FileReader(); + + resultReader.readAsDataURL(file); + + resultReader.addEventListener('load', () => + callback({ file, url: resultReader.result }), + ); + resolve(); + }, + 'image/png', + 1, + ); + }); +} diff --git a/web/cashtab/src/utils/icons/roundImage.js b/web/cashtab/src/utils/icons/roundImage.js new file mode 100644 --- /dev/null +++ b/web/cashtab/src/utils/icons/roundImage.js @@ -0,0 +1,64 @@ +const createImage = url => + new Promise((resolve, reject) => { + const image = new Image(); + image.addEventListener('load', () => resolve(image)); + image.addEventListener('error', error => reject(error)); + image.setAttribute('crossOrigin', 'anonymous'); + image.src = url; + }); + +export default async function getRoundImg(imageSrc, fileName) { + const image = await createImage(imageSrc); + console.log('image :', image); + const canvas = document.createElement('canvas'); + canvas.width = image.width; + canvas.height = image.height; + const ctx = canvas.getContext('2d'); + + ctx.drawImage(image, 0, 0); + ctx.globalCompositeOperation = 'destination-in'; + ctx.beginPath(); + ctx.arc( + image.width / 2, + image.height / 2, + image.height / 2, + 0, + Math.PI * 2, + ); + ctx.closePath(); + ctx.fill(); + if (!HTMLCanvasElement.prototype.toBlob) { + Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { + value: function (callback, type, quality) { + var dataURL = this.toDataURL(type, quality).split(',')[1]; + setTimeout(function () { + var binStr = atob(dataURL), + len = binStr.length, + arr = new Uint8Array(len); + for (var i = 0; i < len; i++) { + arr[i] = binStr.charCodeAt(i); + } + callback(new Blob([arr], { type: type || 'image/png' })); + }); + }, + }); + } + return new Promise(resolve => { + ctx.canvas.toBlob( + blob => { + const file = new File([blob], fileName, { + type: 'image/png', + }); + const resultReader = new FileReader(); + + resultReader.readAsDataURL(file); + + resultReader.addEventListener('load', () => + resolve({ file, url: resultReader.result }), + ); + }, + 'image/png', + 1, + ); + }); +}