forked from treehouse/mastodon
Add focal points support in the composer
parent
9782ac017b
commit
534439e73b
|
@ -211,11 +211,11 @@ export function uploadCompose(files) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function changeUploadCompose(id, description) {
|
export function changeUploadCompose(id, params) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(changeUploadComposeRequest());
|
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));
|
dispatch(changeUploadComposeSuccess(response.data));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(changeUploadComposeFail(id, error));
|
dispatch(changeUploadComposeFail(id, error));
|
||||||
|
|
|
@ -103,7 +103,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||||
dispatch(changeComposeAdvancedOption(option, value));
|
dispatch(changeComposeAdvancedOption(option, value));
|
||||||
},
|
},
|
||||||
onChangeDescription(id, description) {
|
onChangeDescription(id, description) {
|
||||||
dispatch(changeUploadCompose(id, description));
|
dispatch(changeUploadCompose(id, { description }));
|
||||||
},
|
},
|
||||||
onChangeSensitivity() {
|
onChangeSensitivity() {
|
||||||
dispatch(changeComposeSensitivity());
|
dispatch(changeComposeSensitivity());
|
||||||
|
@ -141,6 +141,9 @@ const mapDispatchToProps = (dispatch) => ({
|
||||||
onOpenDoodleModal() {
|
onOpenDoodleModal() {
|
||||||
dispatch(openModal('DOODLE', { noEsc: true }));
|
dispatch(openModal('DOODLE', { noEsc: true }));
|
||||||
},
|
},
|
||||||
|
onOpenFocalPointModal(id) {
|
||||||
|
dispatch(openModal('FOCAL_POINT', { id }));
|
||||||
|
},
|
||||||
onSelectSuggestion(position, token, suggestion) {
|
onSelectSuggestion(position, token, suggestion) {
|
||||||
dispatch(selectComposeSuggestion(position, token, suggestion));
|
dispatch(selectComposeSuggestion(position, token, suggestion));
|
||||||
},
|
},
|
||||||
|
@ -339,6 +342,7 @@ class Composer extends React.Component {
|
||||||
onFetchSuggestions,
|
onFetchSuggestions,
|
||||||
onOpenActionsModal,
|
onOpenActionsModal,
|
||||||
onOpenDoodleModal,
|
onOpenDoodleModal,
|
||||||
|
onOpenFocalPointModal,
|
||||||
onUndoUpload,
|
onUndoUpload,
|
||||||
onUpload,
|
onUpload,
|
||||||
privacy,
|
privacy,
|
||||||
|
@ -397,6 +401,7 @@ class Composer extends React.Component {
|
||||||
intl={intl}
|
intl={intl}
|
||||||
media={media}
|
media={media}
|
||||||
onChangeDescription={onChangeDescription}
|
onChangeDescription={onChangeDescription}
|
||||||
|
onOpenFocalPointModal={onOpenFocalPointModal}
|
||||||
onRemove={onUndoUpload}
|
onRemove={onUndoUpload}
|
||||||
progress={progress}
|
progress={progress}
|
||||||
uploading={isUploading}
|
uploading={isUploading}
|
||||||
|
|
|
@ -13,6 +13,7 @@ export default function ComposerUploadForm ({
|
||||||
intl,
|
intl,
|
||||||
media,
|
media,
|
||||||
onChangeDescription,
|
onChangeDescription,
|
||||||
|
onOpenFocalPointModal,
|
||||||
onRemove,
|
onRemove,
|
||||||
progress,
|
progress,
|
||||||
uploading,
|
uploading,
|
||||||
|
@ -31,8 +32,12 @@ export default function ComposerUploadForm ({
|
||||||
key={item.get('id')}
|
key={item.get('id')}
|
||||||
id={item.get('id')}
|
id={item.get('id')}
|
||||||
intl={intl}
|
intl={intl}
|
||||||
|
focusX={item.getIn(['meta', 'focus', 'x'])}
|
||||||
|
focusY={item.getIn(['meta', 'focus', 'y'])}
|
||||||
|
mediaType={item.get('type')}
|
||||||
preview={item.get('preview_url')}
|
preview={item.get('preview_url')}
|
||||||
onChangeDescription={onChangeDescription}
|
onChangeDescription={onChangeDescription}
|
||||||
|
onOpenFocalPointModal={onOpenFocalPointModal}
|
||||||
onRemove={onRemove}
|
onRemove={onRemove}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -46,8 +51,8 @@ export default function ComposerUploadForm ({
|
||||||
ComposerUploadForm.propTypes = {
|
ComposerUploadForm.propTypes = {
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
media: ImmutablePropTypes.list,
|
media: ImmutablePropTypes.list,
|
||||||
onChangeDescription: PropTypes.func,
|
onChangeDescription: PropTypes.func.isRequired,
|
||||||
onRemove: PropTypes.func,
|
onRemove: PropTypes.func.isRequired,
|
||||||
progress: PropTypes.number,
|
progress: PropTypes.number,
|
||||||
uploading: PropTypes.bool,
|
uploading: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
|
@ -25,6 +25,10 @@ const messages = defineMessages({
|
||||||
defaultMessage: 'Describe for the visually impaired',
|
defaultMessage: 'Describe for the visually impaired',
|
||||||
id: 'upload_form.description',
|
id: 'upload_form.description',
|
||||||
},
|
},
|
||||||
|
crop: {
|
||||||
|
defaultMessage: 'Crop',
|
||||||
|
id: 'upload_form.focus',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handlers.
|
// Handlers.
|
||||||
|
@ -77,6 +81,17 @@ const handlers = {
|
||||||
onRemove(id);
|
onRemove(id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Opens the focal point modal.
|
||||||
|
handleFocalPointClick () {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
onOpenFocalPointModal,
|
||||||
|
} = this.props;
|
||||||
|
if (id && onOpenFocalPointModal) {
|
||||||
|
onOpenFocalPointModal(id);
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// The component.
|
// The component.
|
||||||
|
@ -102,11 +117,15 @@ export default class ComposerUploadFormItem extends React.PureComponent {
|
||||||
handleMouseEnter,
|
handleMouseEnter,
|
||||||
handleMouseLeave,
|
handleMouseLeave,
|
||||||
handleRemove,
|
handleRemove,
|
||||||
|
handleFocalPointClick,
|
||||||
} = this.handlers;
|
} = this.handlers;
|
||||||
const {
|
const {
|
||||||
description,
|
description,
|
||||||
intl,
|
intl,
|
||||||
preview,
|
preview,
|
||||||
|
focusX,
|
||||||
|
focusY,
|
||||||
|
mediaType,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {
|
const {
|
||||||
focused,
|
focused,
|
||||||
|
@ -114,6 +133,8 @@ export default class ComposerUploadFormItem extends React.PureComponent {
|
||||||
dirtyDescription,
|
dirtyDescription,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const computedClass = classNames('composer--upload_form--item', { active: hovered || focused });
|
const computedClass = classNames('composer--upload_form--item', { active: hovered || focused });
|
||||||
|
const x = ((focusX / 2) + .5) * 100;
|
||||||
|
const y = ((focusY / -2) + .5) * 100;
|
||||||
|
|
||||||
// The result.
|
// The result.
|
||||||
return (
|
return (
|
||||||
|
@ -136,15 +157,15 @@ export default class ComposerUploadFormItem extends React.PureComponent {
|
||||||
style={{
|
style={{
|
||||||
transform: `scale(${scale})`,
|
transform: `scale(${scale})`,
|
||||||
backgroundImage: preview ? `url(${preview})` : null,
|
backgroundImage: preview ? `url(${preview})` : null,
|
||||||
|
backgroundPosition: `${x}% ${y}%`
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconButton
|
<div className={classNames('composer--upload_form--actions', { active: hovered || focused })}>
|
||||||
className='close'
|
<button className='icon-button' onClick={handleRemove}>
|
||||||
icon='times'
|
<i className='fa fa-times' /> <FormattedMessage {...messages.undo} />
|
||||||
onClick={handleRemove}
|
</button>
|
||||||
size={36}
|
{mediaType === 'image' && <button className='icon-button' onClick={handleFocalPointClick}><i className='fa fa-crosshairs' /> <FormattedMessage {...messages.crop} /></button>}
|
||||||
title={intl.formatMessage(messages.undo)}
|
</div>
|
||||||
/>
|
|
||||||
<label>
|
<label>
|
||||||
<span style={{ display: 'none' }}><FormattedMessage {...messages.description} /></span>
|
<span style={{ display: 'none' }}><FormattedMessage {...messages.description} /></span>
|
||||||
<input
|
<input
|
||||||
|
@ -171,7 +192,11 @@ ComposerUploadFormItem.propTypes = {
|
||||||
description: PropTypes.string,
|
description: PropTypes.string,
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
onChangeDescription: PropTypes.func,
|
onChangeDescription: PropTypes.func.isRequired,
|
||||||
onRemove: PropTypes.func,
|
onOpenFocalPointModal: PropTypes.func.isRequired,
|
||||||
|
onRemove: PropTypes.func.isRequired,
|
||||||
|
focusX: PropTypes.number,
|
||||||
|
focusY: PropTypes.number,
|
||||||
|
mediaType: PropTypes.string,
|
||||||
preview: 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 FavouriteModal from './favourite_modal';
|
||||||
import DoodleModal from './doodle_modal';
|
import DoodleModal from './doodle_modal';
|
||||||
import ConfirmationModal from './confirmation_modal';
|
import ConfirmationModal from './confirmation_modal';
|
||||||
|
import FocalPointModal from './focal_point_modal';
|
||||||
import {
|
import {
|
||||||
OnboardingModal,
|
OnboardingModal,
|
||||||
MuteModal,
|
MuteModal,
|
||||||
|
@ -34,6 +35,7 @@ const MODAL_COMPONENTS = {
|
||||||
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
||||||
'EMBED': EmbedModal,
|
'EMBED': EmbedModal,
|
||||||
'LIST_EDITOR': ListEditor,
|
'LIST_EDITOR': ListEditor,
|
||||||
|
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class ModalRoot extends React.PureComponent {
|
export default class ModalRoot extends React.PureComponent {
|
||||||
|
|
|
@ -371,7 +371,7 @@ export default function compose(state = initialState, action) {
|
||||||
.set('is_submitting', false)
|
.set('is_submitting', false)
|
||||||
.update('media_attachments', list => list.map(item => {
|
.update('media_attachments', list => list.map(item => {
|
||||||
if (item.get('id') === action.media.id) {
|
if (item.get('id') === action.media.id) {
|
||||||
return item.set('description', action.media.description);
|
return fromJS(action.media);
|
||||||
}
|
}
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
|
|
|
@ -255,11 +255,12 @@
|
||||||
& > div {
|
& > div {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
height: 100px;
|
height: 140px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
display: block;
|
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 {
|
.composer--upload_form--progress {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 10px;
|
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 New Issue