wip: cropping mostly works, uploads do not yet

This commit is contained in:
or-else
2021-07-13 09:35:17 -07:00
parent ab962bfe7d
commit f1196e17ff
9 changed files with 269 additions and 133 deletions
+30 -4
View File
@@ -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>
+41 -3
View File
@@ -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}>
+7 -14
View File
@@ -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>
); );
+7 -7
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long