Add focal points support in the composer
This commit is contained in:
parent
96e58d01ad
commit
02adce1ad8
9 changed files with 240 additions and 16 deletions
|
@ -211,11 +211,11 @@ export function uploadCompose(files) {
|
|||
};
|
||||
};
|
||||
|
||||
export function changeUploadCompose(id, description) {
|
||||
export function changeUploadCompose(id, params) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(changeUploadComposeRequest());
|
||||
|
||||
api(getState).put(`/api/v1/media/${id}`, { description }).then(response => {
|
||||
api(getState).put(`/api/v1/media/${id}`, params).then(response => {
|
||||
dispatch(changeUploadComposeSuccess(response.data));
|
||||
}).catch(error => {
|
||||
dispatch(changeUploadComposeFail(id, error));
|
||||
|
|
|
@ -103,7 +103,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||
dispatch(changeComposeAdvancedOption(option, value));
|
||||
},
|
||||
onChangeDescription(id, description) {
|
||||
dispatch(changeUploadCompose(id, description));
|
||||
dispatch(changeUploadCompose(id, { description }));
|
||||
},
|
||||
onChangeSensitivity() {
|
||||
dispatch(changeComposeSensitivity());
|
||||
|
@ -141,6 +141,9 @@ const mapDispatchToProps = (dispatch) => ({
|
|||
onOpenDoodleModal() {
|
||||
dispatch(openModal('DOODLE', { noEsc: true }));
|
||||
},
|
||||
onOpenFocalPointModal(id) {
|
||||
dispatch(openModal('FOCAL_POINT', { id }));
|
||||
},
|
||||
onSelectSuggestion(position, token, suggestion) {
|
||||
dispatch(selectComposeSuggestion(position, token, suggestion));
|
||||
},
|
||||
|
@ -339,6 +342,7 @@ class Composer extends React.Component {
|
|||
onFetchSuggestions,
|
||||
onOpenActionsModal,
|
||||
onOpenDoodleModal,
|
||||
onOpenFocalPointModal,
|
||||
onUndoUpload,
|
||||
onUpload,
|
||||
privacy,
|
||||
|
@ -397,6 +401,7 @@ class Composer extends React.Component {
|
|||
intl={intl}
|
||||
media={media}
|
||||
onChangeDescription={onChangeDescription}
|
||||
onOpenFocalPointModal={onOpenFocalPointModal}
|
||||
onRemove={onUndoUpload}
|
||||
progress={progress}
|
||||
uploading={isUploading}
|
||||
|
|
|
@ -13,6 +13,7 @@ export default function ComposerUploadForm ({
|
|||
intl,
|
||||
media,
|
||||
onChangeDescription,
|
||||
onOpenFocalPointModal,
|
||||
onRemove,
|
||||
progress,
|
||||
uploading,
|
||||
|
@ -31,8 +32,12 @@ export default function ComposerUploadForm ({
|
|||
key={item.get('id')}
|
||||
id={item.get('id')}
|
||||
intl={intl}
|
||||
focusX={item.getIn(['meta', 'focus', 'x'])}
|
||||
focusY={item.getIn(['meta', 'focus', 'y'])}
|
||||
mediaType={item.get('type')}
|
||||
preview={item.get('preview_url')}
|
||||
onChangeDescription={onChangeDescription}
|
||||
onOpenFocalPointModal={onOpenFocalPointModal}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
))}
|
||||
|
@ -46,8 +51,8 @@ export default function ComposerUploadForm ({
|
|||
ComposerUploadForm.propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
media: ImmutablePropTypes.list,
|
||||
onChangeDescription: PropTypes.func,
|
||||
onRemove: PropTypes.func,
|
||||
onChangeDescription: PropTypes.func.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
progress: PropTypes.number,
|
||||
uploading: PropTypes.bool,
|
||||
};
|
||||
|
|
|
@ -25,6 +25,10 @@ const messages = defineMessages({
|
|||
defaultMessage: 'Describe for the visually impaired',
|
||||
id: 'upload_form.description',
|
||||
},
|
||||
crop: {
|
||||
defaultMessage: 'Crop',
|
||||
id: 'upload_form.focus',
|
||||
},
|
||||
});
|
||||
|
||||
// Handlers.
|
||||
|
@ -77,6 +81,17 @@ const handlers = {
|
|||
onRemove(id);
|
||||
}
|
||||
},
|
||||
|
||||
// Opens the focal point modal.
|
||||
handleFocalPointClick () {
|
||||
const {
|
||||
id,
|
||||
onOpenFocalPointModal,
|
||||
} = this.props;
|
||||
if (id && onOpenFocalPointModal) {
|
||||
onOpenFocalPointModal(id);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// The component.
|
||||
|
@ -102,11 +117,15 @@ export default class ComposerUploadFormItem extends React.PureComponent {
|
|||
handleMouseEnter,
|
||||
handleMouseLeave,
|
||||
handleRemove,
|
||||
handleFocalPointClick,
|
||||
} = this.handlers;
|
||||
const {
|
||||
description,
|
||||
intl,
|
||||
preview,
|
||||
focusX,
|
||||
focusY,
|
||||
mediaType,
|
||||
} = this.props;
|
||||
const {
|
||||
focused,
|
||||
|
@ -114,6 +133,8 @@ export default class ComposerUploadFormItem extends React.PureComponent {
|
|||
dirtyDescription,
|
||||
} = this.state;
|
||||
const computedClass = classNames('composer--upload_form--item', { active: hovered || focused });
|
||||
const x = ((focusX / 2) + .5) * 100;
|
||||
const y = ((focusY / -2) + .5) * 100;
|
||||
|
||||
// The result.
|
||||
return (
|
||||
|
@ -136,15 +157,15 @@ export default class ComposerUploadFormItem extends React.PureComponent {
|
|||
style={{
|
||||
transform: `scale(${scale})`,
|
||||
backgroundImage: preview ? `url(${preview})` : null,
|
||||
backgroundPosition: `${x}% ${y}%`
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
className='close'
|
||||
icon='times'
|
||||
onClick={handleRemove}
|
||||
size={36}
|
||||
title={intl.formatMessage(messages.undo)}
|
||||
/>
|
||||
<div className={classNames('composer--upload_form--actions', { active: hovered || focused })}>
|
||||
<button className='icon-button' onClick={handleRemove}>
|
||||
<i className='fa fa-times' /> <FormattedMessage {...messages.undo} />
|
||||
</button>
|
||||
{mediaType === 'image' && <button className='icon-button' onClick={handleFocalPointClick}><i className='fa fa-crosshairs' /> <FormattedMessage {...messages.crop} /></button>}
|
||||
</div>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}><FormattedMessage {...messages.description} /></span>
|
||||
<input
|
||||
|
@ -171,7 +192,11 @@ ComposerUploadFormItem.propTypes = {
|
|||
description: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onChangeDescription: PropTypes.func,
|
||||
onRemove: PropTypes.func,
|
||||
onChangeDescription: PropTypes.func.isRequired,
|
||||
onOpenFocalPointModal: PropTypes.func.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
focusX: PropTypes.number,
|
||||
focusY: PropTypes.number,
|
||||
mediaType: PropTypes.string,
|
||||
preview: PropTypes.string,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
import ImageLoader from './image_loader';
|
||||
import classNames from 'classnames';
|
||||
import { changeUploadCompose } from 'flavours/glitch/actions/compose';
|
||||
import { getPointerPosition } from 'flavours/glitch/features/video';
|
||||
|
||||
const mapStateToProps = (state, { id }) => ({
|
||||
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { id }) => ({
|
||||
|
||||
onSave: (x, y) => {
|
||||
dispatch(changeUploadCompose(id, { focus: `${x.toFixed(2)},${y.toFixed(2)}` }));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@connect(mapStateToProps, mapDispatchToProps)
|
||||
export default class FocalPointModal extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
media: ImmutablePropTypes.map.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
focusX: 0,
|
||||
focusY: 0,
|
||||
dragging: false,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
this.updatePositionFromMedia(this.props.media);
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (this.props.media.get('id') !== nextProps.media.get('id')) {
|
||||
this.updatePositionFromMedia(nextProps.media);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
document.removeEventListener('mousemove', this.handleMouseMove);
|
||||
document.removeEventListener('mouseup', this.handleMouseUp);
|
||||
}
|
||||
|
||||
handleMouseDown = e => {
|
||||
document.addEventListener('mousemove', this.handleMouseMove);
|
||||
document.addEventListener('mouseup', this.handleMouseUp);
|
||||
|
||||
this.updatePosition(e);
|
||||
this.setState({ dragging: true });
|
||||
}
|
||||
|
||||
handleMouseMove = e => {
|
||||
this.updatePosition(e);
|
||||
}
|
||||
|
||||
handleMouseUp = () => {
|
||||
document.removeEventListener('mousemove', this.handleMouseMove);
|
||||
document.removeEventListener('mouseup', this.handleMouseUp);
|
||||
|
||||
this.setState({ dragging: false });
|
||||
this.props.onSave(this.state.focusX, this.state.focusY);
|
||||
}
|
||||
|
||||
updatePosition = e => {
|
||||
const { x, y } = getPointerPosition(this.node, e);
|
||||
const focusX = (x - .5) * 2;
|
||||
const focusY = (y - .5) * -2;
|
||||
|
||||
this.setState({ x, y, focusX, focusY });
|
||||
}
|
||||
|
||||
updatePositionFromMedia = media => {
|
||||
const focusX = media.getIn(['meta', 'focus', 'x']);
|
||||
const focusY = media.getIn(['meta', 'focus', 'y']);
|
||||
|
||||
if (focusX && focusY) {
|
||||
const x = (focusX / 2) + .5;
|
||||
const y = (focusY / -2) + .5;
|
||||
|
||||
this.setState({ x, y, focusX, focusY });
|
||||
} else {
|
||||
this.setState({ x: 0.5, y: 0.5, focusX: 0, focusY: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.node = c;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { media } = this.props;
|
||||
const { x, y, dragging } = this.state;
|
||||
|
||||
const width = media.getIn(['meta', 'original', 'width']) || null;
|
||||
const height = media.getIn(['meta', 'original', 'height']) || null;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal video-modal focal-point-modal'>
|
||||
<div className={classNames('focal-point', { dragging })} ref={this.setRef}>
|
||||
<ImageLoader
|
||||
previewSrc={media.get('preview_url')}
|
||||
src={media.get('url')}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
|
||||
<div className='focal-point__reticle' style={{ top: `${y * 100}%`, left: `${x * 100}%` }} />
|
||||
<div className='focal-point__overlay' onMouseDown={this.handleMouseDown} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -11,6 +11,7 @@ import BoostModal from './boost_modal';
|
|||
import FavouriteModal from './favourite_modal';
|
||||
import DoodleModal from './doodle_modal';
|
||||
import ConfirmationModal from './confirmation_modal';
|
||||
import FocalPointModal from './focal_point_modal';
|
||||
import {
|
||||
OnboardingModal,
|
||||
MuteModal,
|
||||
|
@ -34,6 +35,7 @@ const MODAL_COMPONENTS = {
|
|||
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
||||
'EMBED': EmbedModal,
|
||||
'LIST_EDITOR': ListEditor,
|
||||
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
|
||||
};
|
||||
|
||||
export default class ModalRoot extends React.PureComponent {
|
||||
|
|
|
@ -371,7 +371,7 @@ export default function compose(state = initialState, action) {
|
|||
.set('is_submitting', false)
|
||||
.update('media_attachments', list => list.map(item => {
|
||||
if (item.get('id') === action.media.id) {
|
||||
return item.set('description', action.media.description);
|
||||
return fromJS(action.media);
|
||||
}
|
||||
|
||||
return item;
|
||||
|
|
|
@ -255,11 +255,12 @@
|
|||
& > div {
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
height: 100px;
|
||||
height: 140px;
|
||||
width: 100%;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
overflow: hidden;
|
||||
|
||||
input {
|
||||
display: block;
|
||||
|
@ -298,6 +299,34 @@
|
|||
}
|
||||
}
|
||||
|
||||
.composer--upload_form--actions {
|
||||
background: linear-gradient(180deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
opacity: 0;
|
||||
transition: opacity .1s ease;
|
||||
|
||||
.icon-button {
|
||||
flex: 0 1 auto;
|
||||
color: $ui-secondary-color;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
padding: 10px;
|
||||
font-family: inherit;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
color: lighten($ui-secondary-color, 4%);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.composer--upload_form--progress {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
|
|
|
@ -763,3 +763,39 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.focal-point {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
|
||||
&.dragging {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 80vw;
|
||||
max-height: 80vh;
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
&__reticle {
|
||||
position: absolute;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
transform: translate(-50%, -50%);
|
||||
background: url('~/images/reticle.png') no-repeat 0 0;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 9999em rgba($base-shadow-color, 0.35);
|
||||
}
|
||||
|
||||
&__overlay {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue