wip: cropping mostly works, uploads do not yet
This commit is contained in:
@@ -4,6 +4,8 @@ import { FormattedMessage } from 'react-intl';
|
|||||||
|
|
||||||
import Tinode from 'tinode-sdk';
|
import Tinode from 'tinode-sdk';
|
||||||
|
|
||||||
|
import AvatarCropView from './avatar-crop-view.jsx';
|
||||||
|
|
||||||
import AvatarUpload from '../widgets/avatar-upload.jsx';
|
import AvatarUpload from '../widgets/avatar-upload.jsx';
|
||||||
import InPlaceEdit from '../widgets/in-place-edit.jsx';
|
import InPlaceEdit from '../widgets/in-place-edit.jsx';
|
||||||
import TagManager from '../widgets/tag-manager.jsx';
|
import TagManager from '../widgets/tag-manager.jsx';
|
||||||
@@ -25,13 +27,17 @@ export default class AccGeneralView extends React.Component {
|
|||||||
addCredActive: false,
|
addCredActive: false,
|
||||||
addCredInvalid: false,
|
addCredInvalid: false,
|
||||||
newCred: '',
|
newCred: '',
|
||||||
|
newAvatar: null,
|
||||||
|
newAvatarMime: null,
|
||||||
previousOnTags: me.onTagsUpdated
|
previousOnTags: me.onTagsUpdated
|
||||||
};
|
};
|
||||||
|
|
||||||
this.tnNewTags = this.tnNewTags.bind(this);
|
this.tnNewTags = this.tnNewTags.bind(this);
|
||||||
this.tnCredsUpdated = this.tnCredsUpdated.bind(this);
|
this.tnCredsUpdated = this.tnCredsUpdated.bind(this);
|
||||||
this.handleFullNameUpdate = this.handleFullNameUpdate.bind(this);
|
this.handleFullNameUpdate = this.handleFullNameUpdate.bind(this);
|
||||||
this.handleImageChanged = this.handleImageChanged.bind(this);
|
this.handleImageUploaded = this.handleImageUploaded.bind(this);
|
||||||
|
this.handleAvatarCropped = this.handleAvatarCropped.bind(this);
|
||||||
|
this.handleCancel = this.handleCancel.bind(this);
|
||||||
this.handleCredChange = this.handleCredChange.bind(this);
|
this.handleCredChange = this.handleCredChange.bind(this);
|
||||||
this.handleCredKeyDown = this.handleCredKeyDown.bind(this);
|
this.handleCredKeyDown = this.handleCredKeyDown.bind(this);
|
||||||
this.handleCredEntered = this.handleCredEntered.bind(this);
|
this.handleCredEntered = this.handleCredEntered.bind(this);
|
||||||
@@ -66,11 +72,20 @@ export default class AccGeneralView extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleImageChanged(img) {
|
handleImageUploaded(mime, img) {
|
||||||
this.setState({avatar: img});
|
this.setState({newAvatar: img, newAvatarMime: mime});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAvatarCropped(mime, blob, width, height) {
|
||||||
|
const url = blob ? URL.createObjectURL(blob) : null;
|
||||||
|
this.setState({avatar: url, newAvatar: null, newAvatarMime: null});
|
||||||
// this.props.onUpdateAccount(undefined, theCard(null, img || Tinode.DEL_CHAR));
|
// this.props.onUpdateAccount(undefined, theCard(null, img || Tinode.DEL_CHAR));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleCancel(img) {
|
||||||
|
this.setState({newAvatar: null, newAvatarMime: null});
|
||||||
|
}
|
||||||
|
|
||||||
handleCredChange(e) {
|
handleCredChange(e) {
|
||||||
this.setState({newCred: e.target.value, addCredInvalid: false});
|
this.setState({newCred: e.target.value, addCredInvalid: false});
|
||||||
}
|
}
|
||||||
@@ -119,6 +134,17 @@ export default class AccGeneralView extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
if (this.state.newAvatar) {
|
||||||
|
return (
|
||||||
|
<AvatarCropView
|
||||||
|
avatar={this.state.newAvatar}
|
||||||
|
mime={this.state.newAvatarMime}
|
||||||
|
onSubmit={this.handleAvatarCropped}
|
||||||
|
onCancel={this.handleCancel}
|
||||||
|
onError={this.props.onError} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const credentials = [];
|
const credentials = [];
|
||||||
this.state.credentials.map((cred) => {
|
this.state.credentials.map((cred) => {
|
||||||
credentials.push(<div key={cred.meth + ":" + cred.val + ":" + cred.done}>{cred.meth}: <tt>{cred.val}</tt>
|
credentials.push(<div key={cred.meth + ":" + cred.val + ":" + cred.done}>{cred.meth}: <tt>{cred.val}</tt>
|
||||||
@@ -150,7 +176,7 @@ export default class AccGeneralView extends React.Component {
|
|||||||
avatar={this.state.avatar}
|
avatar={this.state.avatar}
|
||||||
uid={this.props.myUserId}
|
uid={this.props.myUserId}
|
||||||
title={this.state.fullName}
|
title={this.state.fullName}
|
||||||
onImageReceived={this.props.onImageReceived}
|
onImageReceived={this.handleImageUploaded}
|
||||||
onImageChanged={this.handleImageChanged}
|
onImageChanged={this.handleImageChanged}
|
||||||
onError={this.props.onError} />
|
onError={this.props.onError} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,25 +3,63 @@ import React from 'react';
|
|||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import Cropper from '../widgets/cropper.jsx';
|
import Cropper from '../widgets/cropper.jsx';
|
||||||
|
import { imageCrop } from '../lib/blob-helpers.js';
|
||||||
|
|
||||||
|
/*
|
||||||
|
crop: {
|
||||||
|
id: 'sidepanel_title_crop',
|
||||||
|
description: 'Sidepanel title for AvatarCropView.',
|
||||||
|
defaultMessage: 'Drag to Adjust'
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
|
||||||
export default class AvatarCropView extends React.PureComponent {
|
export default class AvatarCropView extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
scale: 1
|
||||||
|
};
|
||||||
|
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
this.handleChange = this.handleChange.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cut out in image coordinates and desired scale factor.
|
||||||
|
handleChange(top, left, width, height, scale) {
|
||||||
|
this.setState({
|
||||||
|
top: top,
|
||||||
|
left: left,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
scale: scale
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSubmit() {
|
handleSubmit() {
|
||||||
if (this.props.onSubmit) {
|
console.log(this.state);
|
||||||
this.props.onSubmit();
|
imageCrop(this.props.mime, this.props.avatar, this.state.top,
|
||||||
|
this.state.left, this.state.width, this.state.height, this.state.scale,
|
||||||
|
(mime, blob, width, height) => {
|
||||||
|
this.props.onSubmit(mime, blob, width, height);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
this.props.onError(err);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="panel-form">
|
<div className="panel-form">
|
||||||
<div className="panel-form-row">
|
<div className="panel-form-row">
|
||||||
<Cropper source={this.props.avatar} />
|
<Cropper
|
||||||
|
source={this.props.avatar}
|
||||||
|
onChange={this.handleChange} />
|
||||||
</div>
|
</div>
|
||||||
<div className="dialog-buttons">
|
<div className="dialog-buttons">
|
||||||
<button className="primary" onClick={this.handleSubmit}>
|
<button className="primary" onClick={this.handleSubmit}>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import ErrorPanel from '../widgets/error-panel.jsx';
|
|||||||
import LoadSpinner from '../widgets/load-spinner.jsx';
|
import LoadSpinner from '../widgets/load-spinner.jsx';
|
||||||
import SideNavbar from '../widgets/side-navbar.jsx';
|
import SideNavbar from '../widgets/side-navbar.jsx';
|
||||||
|
|
||||||
import AvatarCropView from './avatar-crop-view.jsx';
|
|
||||||
import ContactsView from './contacts-view.jsx';
|
import ContactsView from './contacts-view.jsx';
|
||||||
import CreateAccountView from './create-account-view.jsx';
|
import CreateAccountView from './create-account-view.jsx';
|
||||||
import EditAccountView from './edit-account-view.jsx';
|
import EditAccountView from './edit-account-view.jsx';
|
||||||
@@ -51,6 +50,11 @@ const messages = defineMessages({
|
|||||||
description: 'Sidepanel title for AccSecurityView.',
|
description: 'Sidepanel title for AccSecurityView.',
|
||||||
defaultMessage: 'Security'
|
defaultMessage: 'Security'
|
||||||
},
|
},
|
||||||
|
crop: {
|
||||||
|
id: 'sidepanel_title_crop',
|
||||||
|
description: 'Sidepanel title for AvatarCropView.',
|
||||||
|
defaultMessage: 'Drag to Adjust'
|
||||||
|
},
|
||||||
notif: {
|
notif: {
|
||||||
id: 'sidepanel_title_acc_notifications',
|
id: 'sidepanel_title_acc_notifications',
|
||||||
description: 'Sidepanel title for AccNotificationsView.',
|
description: 'Sidepanel title for AccNotificationsView.',
|
||||||
@@ -71,11 +75,6 @@ const messages = defineMessages({
|
|||||||
description: 'Sidepanel title for ValidationView.',
|
description: 'Sidepanel title for ValidationView.',
|
||||||
defaultMessage: 'Confirm Credentials'
|
defaultMessage: 'Confirm Credentials'
|
||||||
},
|
},
|
||||||
crop: {
|
|
||||||
id: 'sidepanel_title_crop',
|
|
||||||
description: 'Sidepanel title for AvatarCropView.',
|
|
||||||
defaultMessage: 'Drag to Adjust'
|
|
||||||
},
|
|
||||||
reset: {
|
reset: {
|
||||||
id: 'sidepanel_title_reset',
|
id: 'sidepanel_title_reset',
|
||||||
description: 'Sidepanel title for PasswordResetView.',
|
description: 'Sidepanel title for PasswordResetView.',
|
||||||
@@ -175,16 +174,16 @@ class SidepanelView extends React.Component {
|
|||||||
trustedBadges={this.props.trustedBadges}
|
trustedBadges={this.props.trustedBadges}
|
||||||
onBasicNavigate={this.props.onBasicNavigate} /> :
|
onBasicNavigate={this.props.onBasicNavigate} /> :
|
||||||
|
|
||||||
view === 'general' ?
|
(view === 'general' || view === 'crop') ?
|
||||||
<AccGeneralView
|
<AccGeneralView
|
||||||
tinode={this.props.tinode}
|
tinode={this.props.tinode}
|
||||||
myUserId={this.props.myUserId}
|
myUserId={this.props.myUserId}
|
||||||
|
onBasicNavigate={this.props.onBasicNavigate}
|
||||||
onUpdateAccount={this.props.onUpdateAccount}
|
onUpdateAccount={this.props.onUpdateAccount}
|
||||||
onUpdateTags={this.props.onUpdateAccountTags}
|
onUpdateTags={this.props.onUpdateAccountTags}
|
||||||
onCredAdd={this.props.onCredAdd}
|
onCredAdd={this.props.onCredAdd}
|
||||||
onCredDelete={this.props.onCredDelete}
|
onCredDelete={this.props.onCredDelete}
|
||||||
onCredConfirm={this.props.onCredConfirm}
|
onCredConfirm={this.props.onCredConfirm}
|
||||||
onImageReceived={this.props.onImageReceived}
|
|
||||||
onError={this.props.onError} /> :
|
onError={this.props.onError} /> :
|
||||||
|
|
||||||
view === 'notif' ?
|
view === 'notif' ?
|
||||||
@@ -246,12 +245,6 @@ class SidepanelView extends React.Component {
|
|||||||
onRequest={this.props.onPasswordResetRequest}
|
onRequest={this.props.onPasswordResetRequest}
|
||||||
onReset={this.props.onResetPassword} /> :
|
onReset={this.props.onResetPassword} /> :
|
||||||
|
|
||||||
view === 'crop' ?
|
|
||||||
<AvatarCropView
|
|
||||||
avatar={this.props.newAvatar}
|
|
||||||
onSubmit={this.props.onAvatarCropped}
|
|
||||||
onCancel={this.props.onCancel} /> :
|
|
||||||
|
|
||||||
null}
|
null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default class AvatarUpload extends React.Component {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
dataUrl: props.avatar,
|
source: props.avatar,
|
||||||
uploading: false
|
uploading: false
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -21,14 +21,14 @@ export default class AvatarUpload extends React.Component {
|
|||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (this.props.avatar != prevProps.avatar) {
|
if (this.props.avatar != prevProps.avatar) {
|
||||||
this.setState({dataUrl: this.props.avatar});
|
this.setState({source: this.props.avatar});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFileUpload(e) {
|
handleFileUpload(e) {
|
||||||
const image = e.target.files[0];
|
const image = e.target.files[0];
|
||||||
if (this.props.onImageReceived) {
|
if (this.props.onImageReceived) {
|
||||||
this.props.onImageReceived(URL.createObjectURL(image));
|
this.props.onImageReceived(image.type, URL.createObjectURL(image));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ export default class AvatarUpload extends React.Component {
|
|||||||
// Convert blob to base64-encoded bits.
|
// Convert blob to base64-encoded bits.
|
||||||
blobToBase64(blob, (unused, base64bits) => {
|
blobToBase64(blob, (unused, base64bits) => {
|
||||||
const du = makeImageDataUrl({data: base64bits, type: mime});
|
const du = makeImageDataUrl({data: base64bits, type: mime});
|
||||||
this.setState({dataUrl: du});
|
this.setState({source: du});
|
||||||
this.props.onImageChanged(du);
|
this.props.onImageChanged(du);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -77,13 +77,13 @@ export default class AvatarUpload extends React.Component {
|
|||||||
const className = 'avatar-upload' + (this.props.readOnly ? ' read-only' : '');
|
const className = 'avatar-upload' + (this.props.readOnly ? ' read-only' : '');
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{this.props.readOnly || !this.state.dataUrl ?
|
{this.props.readOnly || !this.state.source ?
|
||||||
null :
|
null :
|
||||||
<a href="#" className="clear-avatar" onClick={(e) => {e.preventDefault(); this.props.onImageChanged(null);}}>
|
<a href="#" className="clear-avatar" onClick={(e) => {e.preventDefault(); this.props.onImageChanged(null);}}>
|
||||||
<i className="material-icons">clear</i>
|
<i className="material-icons">clear</i>
|
||||||
</a>}
|
</a>}
|
||||||
{this.state.dataUrl ?
|
{this.state.source ?
|
||||||
<img src={this.state.dataUrl} className="preview" /> :
|
<img src={this.state.source} className="preview" /> :
|
||||||
this.props.readOnly && this.props.uid ?
|
this.props.readOnly && this.props.uid ?
|
||||||
<div className="avatar-box">
|
<div className="avatar-box">
|
||||||
<LetterTile
|
<LetterTile
|
||||||
|
|||||||
+15
-5
@@ -50,6 +50,7 @@ export default class Cropper extends React.Component {
|
|||||||
this.originY = 0;
|
this.originY = 0;
|
||||||
|
|
||||||
this.initScaling = this.initScaling.bind(this);
|
this.initScaling = this.initScaling.bind(this);
|
||||||
|
this.onZoom = this.onZoom.bind(this);
|
||||||
this.handleZoom = this.handleZoom.bind(this);
|
this.handleZoom = this.handleZoom.bind(this);
|
||||||
this.mouseDown = this.mouseDown.bind(this);
|
this.mouseDown = this.mouseDown.bind(this);
|
||||||
this.mouseUp = this.mouseUp.bind(this);
|
this.mouseUp = this.mouseUp.bind(this);
|
||||||
@@ -126,9 +127,11 @@ export default class Cropper extends React.Component {
|
|||||||
this.positionAll(panX, panY, zoom);
|
this.positionAll(panX, panY, zoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleZoom(e) {
|
onZoom(e) {
|
||||||
let zoom = e.target.value;
|
this.handleZoom(e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleZoom(zoom) {
|
||||||
let panX = this.state.panX;
|
let panX = this.state.panX;
|
||||||
let panY = this.state.panY;
|
let panY = this.state.panY;
|
||||||
|
|
||||||
@@ -217,8 +220,8 @@ export default class Cropper extends React.Component {
|
|||||||
this.prevDistance = distance / this.state.zoom;
|
this.prevDistance = distance / this.state.zoom;
|
||||||
}
|
}
|
||||||
|
|
||||||
let scale = (distance / this.prevDistance).toFixed(3);
|
let scale = (distance / this.prevDistance);
|
||||||
this.setState({zoom: Math.max(this.minZoom, Math.min(this.maxZoom, scale))});
|
this.handleZoom(Math.max(this.minZoom, Math.min(this.maxZoom, scale)));
|
||||||
}
|
}
|
||||||
|
|
||||||
mouseUp(e) {
|
mouseUp(e) {
|
||||||
@@ -230,6 +233,13 @@ export default class Cropper extends React.Component {
|
|||||||
document.body.style['userSelect'] = '';
|
document.body.style['userSelect'] = '';
|
||||||
|
|
||||||
this.positionAll(this.state.panX, this.state.panY, this.state.zoom);
|
this.positionAll(this.state.panX, this.state.panY, this.state.zoom);
|
||||||
|
// Pass cut out coordinates to caller.
|
||||||
|
this.props.onChange(
|
||||||
|
-this.state.panX + this.cutoutRect.left - this.bBoxRect.left,
|
||||||
|
-this.state.panY + this.cutoutRect.top - this.bBoxRect.top,
|
||||||
|
this.cutoutRect.width / this.state.zoom,
|
||||||
|
this.cutoutRect.height / this.state.zoom,
|
||||||
|
this.state.zoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -256,7 +266,7 @@ export default class Cropper extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
<div className="zoom-wrapper">
|
<div className="zoom-wrapper">
|
||||||
<input type="range" className="zoomer"
|
<input type="range" className="zoomer"
|
||||||
step="0.001" min={this.state.minZoom} max={this.state.maxZoom} value={this.state.zoom} onChange={this.handleZoom} />
|
step="0.001" min={this.state.minZoom} max={this.state.maxZoom} value={this.state.zoom} onChange={this.onZoom} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+165
-96
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user