diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb
index 9f330f0df..d4e6337e7 100644
--- a/app/controllers/api/v1/media_controller.rb
+++ b/app/controllers/api/v1/media_controller.rb
@@ -27,7 +27,7 @@ class Api::V1::MediaController < Api::BaseController
private
def media_params
- params.permit(:file, :description)
+ params.permit(:file, :description, :focus)
end
def file_type_error
diff --git a/app/javascript/images/reticle.png b/app/javascript/images/reticle.png
new file mode 100644
index 000000000..998994f5c
Binary files /dev/null and b/app/javascript/images/reticle.png differ
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index 8a35049b3..1732ff189 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -178,11 +178,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));
diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js
index a3ffc45ea..9e1bb77c2 100644
--- a/app/javascript/mastodon/components/media_gallery.js
+++ b/app/javascript/mastodon/components/media_gallery.js
@@ -12,6 +12,26 @@ const messages = defineMessages({
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
});
+const shiftToPoint = (containerToImageRatio, containerSize, imageSize, focusSize, toMinus) => {
+ const containerCenter = Math.floor(containerSize / 2);
+ const focusFactor = (focusSize + 1) / 2;
+ const scaledImage = Math.floor(imageSize / containerToImageRatio);
+
+ let focus = Math.floor(focusFactor * scaledImage);
+
+ if (toMinus) focus = scaledImage - focus;
+
+ let focusOffset = focus - containerCenter;
+
+ const remainder = scaledImage - focus;
+ const containerRemainder = containerSize - containerCenter;
+
+ if (remainder < containerRemainder) focusOffset -= containerRemainder - remainder;
+ if (focusOffset < 0) focusOffset = 0;
+
+ return (focusOffset * -100 / containerSize) + '%';
+};
+
class Item extends React.PureComponent {
static contextTypes = {
@@ -24,6 +44,8 @@ class Item extends React.PureComponent {
index: PropTypes.number.isRequired,
size: PropTypes.number.isRequired,
onClick: PropTypes.func.isRequired,
+ containerWidth: PropTypes.number,
+ containerHeight: PropTypes.number,
};
static defaultProps = {
@@ -62,7 +84,7 @@ class Item extends React.PureComponent {
}
render () {
- const { attachment, index, size, standalone } = this.props;
+ const { attachment, index, size, standalone, containerWidth, containerHeight } = this.props;
let width = 50;
let height = 100;
@@ -116,16 +138,40 @@ class Item extends React.PureComponent {
let thumbnail = '';
if (attachment.get('type') === 'image') {
- const previewUrl = attachment.get('preview_url');
+ const previewUrl = attachment.get('preview_url');
const previewWidth = attachment.getIn(['meta', 'small', 'width']);
- const originalUrl = attachment.get('url');
- const originalWidth = attachment.getIn(['meta', 'original', 'width']);
+ const originalUrl = attachment.get('url');
+ const originalWidth = attachment.getIn(['meta', 'original', 'width']);
+ const originalHeight = attachment.getIn(['meta', 'original', 'height']);
const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number';
const srcSet = hasSize ? `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w` : null;
- const sizes = hasSize ? `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw` : null;
+ const sizes = hasSize ? `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw` : null;
+
+ const focusX = attachment.getIn(['meta', 'focus', 'x']);
+ const focusY = attachment.getIn(['meta', 'focus', 'y']);
+ const imageStyle = {};
+
+ if (originalWidth && originalHeight && containerWidth && containerHeight && focusX && focusY) {
+ const widthRatio = originalWidth / (containerWidth * (width / 100));
+ const heightRatio = originalHeight / (containerHeight * (height / 100));
+
+ let hShift = 0;
+ let vShift = 0;
+
+ if (widthRatio > heightRatio) {
+ hShift = shiftToPoint(heightRatio, (containerWidth * (width / 100)), originalWidth, focusX);
+ } else if(widthRatio < heightRatio) {
+ vShift = shiftToPoint(widthRatio, (containerHeight * (height / 100)), originalHeight, focusY, true);
+ }
+
+ imageStyle.top = vShift;
+ imageStyle.left = hShift;
+ } else {
+ imageStyle.height = '100%';
+ }
thumbnail = (
-
+
);
} else if (attachment.get('type') === 'gifv') {
@@ -205,7 +258,7 @@ export default class MediaGallery extends React.PureComponent {
}
handleRef = (node) => {
- if (node && this.isStandaloneEligible()) {
+ if (node /*&& this.isStandaloneEligible()*/) {
// offsetWidth triggers a layout, so only calculate when we need to
this.setState({
width: node.offsetWidth,
@@ -256,12 +309,12 @@ export default class MediaGallery extends React.PureComponent {
if (this.isStandaloneEligible()) {
children = ;
} else {
- children = media.take(4).map((attachment, i) => );
+ children = media.take(4).map((attachment, i) => );
}
}
return (
-
+
diff --git a/app/javascript/mastodon/features/compose/components/upload.js b/app/javascript/mastodon/features/compose/components/upload.js
index 3a3d17710..61b2d19e0 100644
--- a/app/javascript/mastodon/features/compose/components/upload.js
+++ b/app/javascript/mastodon/features/compose/components/upload.js
@@ -1,15 +1,13 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
-import IconButton from '../../../components/icon_button';
import Motion from '../../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import { defineMessages, injectIntl } from 'react-intl';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
const messages = defineMessages({
- undo: { id: 'upload_form.undo', defaultMessage: 'Undo' },
description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' },
});
@@ -21,6 +19,7 @@ export default class Upload extends ImmutablePureComponent {
intl: PropTypes.object.isRequired,
onUndo: PropTypes.func.isRequired,
onDescriptionChange: PropTypes.func.isRequired,
+ onOpenFocalPoint: PropTypes.func.isRequired,
};
state = {
@@ -33,6 +32,10 @@ export default class Upload extends ImmutablePureComponent {
this.props.onUndo(this.props.media.get('id'));
}
+ handleFocalPointClick = () => {
+ this.props.onOpenFocalPoint(this.props.media.get('id'));
+ }
+
handleInputChange = e => {
this.setState({ dirtyDescription: e.target.value });
}
@@ -63,13 +66,20 @@ export default class Upload extends ImmutablePureComponent {
const { intl, media } = this.props;
const active = this.state.hovered || this.state.focused;
const description = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || '';
+ const focusX = media.getIn(['meta', 'focus', 'x']);
+ const focusY = media.getIn(['meta', 'focus', 'y']);
+ const x = ((focusX / 2) + .5) * 100;
+ const y = ((focusY / -2) + .5) * 100;
return (
{({ scale }) => (
-
-
+
+
+
+ {media.get('type') === 'image' && }
+