about summary refs log tree commit diff
path: root/app/javascript
diff options
context:
space:
mode:
authorThibaut Girka <thib@sitedethib.com>2018-08-18 11:01:53 +0200
committerThibG <thib@sitedethib.com>2018-08-18 17:53:20 +0200
commit534439e73b95814b0db927052c9522c60fc306c5 (patch)
tree5a921e8053e211da9b7d38326da18708a87a5b1c /app/javascript
parent9782ac017bbee51f443378350480c864a268ac08 (diff)
Add focal points support in the composer
Diffstat (limited to 'app/javascript')
-rw-r--r--app/javascript/flavours/glitch/actions/compose.js4
-rw-r--r--app/javascript/flavours/glitch/features/composer/index.js7
-rw-r--r--app/javascript/flavours/glitch/features/composer/upload_form/index.js9
-rw-r--r--app/javascript/flavours/glitch/features/composer/upload_form/item/index.js43
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js122
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/modal_root.js2
-rw-r--r--app/javascript/flavours/glitch/reducers/compose.js2
-rw-r--r--app/javascript/flavours/glitch/styles/components/composer.scss31
-rw-r--r--app/javascript/flavours/glitch/styles/components/modal.scss36
9 files changed, 240 insertions, 16 deletions
diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js
index b7f706a83..f6c8086fe 100644
--- a/app/javascript/flavours/glitch/actions/compose.js
+++ b/app/javascript/flavours/glitch/actions/compose.js
@@ -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));
diff --git a/app/javascript/flavours/glitch/features/composer/index.js b/app/javascript/flavours/glitch/features/composer/index.js
index 771c5d0e3..e77d429be 100644
--- a/app/javascript/flavours/glitch/features/composer/index.js
+++ b/app/javascript/flavours/glitch/features/composer/index.js
@@ -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}
diff --git a/app/javascript/flavours/glitch/features/composer/upload_form/index.js b/app/javascript/flavours/glitch/features/composer/upload_form/index.js
index 53b14acc7..f3cadc2f5 100644
--- a/app/javascript/flavours/glitch/features/composer/upload_form/index.js
+++ b/app/javascript/flavours/glitch/features/composer/upload_form/index.js
@@ -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,
 };
diff --git a/app/javascript/flavours/glitch/features/composer/upload_form/item/index.js b/app/javascript/flavours/glitch/features/composer/upload_form/item/index.js
index ec67b8ef8..b9986588f 100644
--- a/app/javascript/flavours/glitch/features/composer/upload_form/item/index.js
+++ b/app/javascript/flavours/glitch/features/composer/upload_form/item/index.js
@@ -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,
 };
diff --git a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js
new file mode 100644
index 000000000..57c92cc66
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js
@@ -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>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.js b/app/javascript/flavours/glitch/features/ui/components/modal_root.js
index e54ab9a52..23a7603d8 100644
--- a/app/javascript/flavours/glitch/features/ui/components/modal_root.js
+++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.js
@@ -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 {
diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js
index 53dd65b07..8b997bf4d 100644
--- a/app/javascript/flavours/glitch/reducers/compose.js
+++ b/app/javascript/flavours/glitch/reducers/compose.js
@@ -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;
diff --git a/app/javascript/flavours/glitch/styles/components/composer.scss b/app/javascript/flavours/glitch/styles/components/composer.scss
index 77ba34672..fab94d8c3 100644
--- a/app/javascript/flavours/glitch/styles/components/composer.scss
+++ b/app/javascript/flavours/glitch/styles/components/composer.scss
@@ -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;
diff --git a/app/javascript/flavours/glitch/styles/components/modal.scss b/app/javascript/flavours/glitch/styles/components/modal.scss
index 49ed47440..1bfedc383 100644
--- a/app/javascript/flavours/glitch/styles/components/modal.scss
+++ b/app/javascript/flavours/glitch/styles/components/modal.scss
@@ -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;
+  }
+}