From 6b52ed4dfadd468d96ee905e65da2955f3212824 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Sat, 21 Jan 2017 14:44:14 +0100 Subject: [PATCH] Adjust the location of the signer snippet (#4155) * Move signer tab * WIP * Better movments * Save Parity Bar position per dapp * Add position to Dapp Manifest * Stick Parity Bar to the four corners --- js/src/ui/Icons/index.js | 4 + .../ui/ParityBackground/parityBackground.js | 13 +- js/src/views/Dapps/builtin.json | 1 + js/src/views/Dapps/dappsStore.js | 25 +- js/src/views/ParityBar/parityBar.css | 137 ++++-- js/src/views/ParityBar/parityBar.js | 412 ++++++++++++++++-- 6 files changed, 497 insertions(+), 95 deletions(-) diff --git a/js/src/ui/Icons/index.js b/js/src/ui/Icons/index.js index a35bbb610..fc3b67391 100644 --- a/js/src/ui/Icons/index.js +++ b/js/src/ui/Icons/index.js @@ -25,8 +25,10 @@ import DashboardIcon from 'material-ui/svg-icons/action/dashboard'; import DeleteIcon from 'material-ui/svg-icons/action/delete'; import DoneIcon from 'material-ui/svg-icons/action/done-all'; import EditIcon from 'material-ui/svg-icons/content/create'; +import FingerprintIcon from 'material-ui/svg-icons/action/fingerprint'; import LinkIcon from 'material-ui/svg-icons/content/link'; import LockedIcon from 'material-ui/svg-icons/action/lock'; +import MoveIcon from 'material-ui/svg-icons/action/open-with'; import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward'; import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back'; import SaveIcon from 'material-ui/svg-icons/content/save'; @@ -48,8 +50,10 @@ export { DeleteIcon, DoneIcon, EditIcon, + FingerprintIcon, LinkIcon, LockedIcon, + MoveIcon, NextIcon, PrevIcon, SaveIcon, diff --git a/js/src/ui/ParityBackground/parityBackground.js b/js/src/ui/ParityBackground/parityBackground.js index 6e554b846..cab3f31d3 100644 --- a/js/src/ui/ParityBackground/parityBackground.js +++ b/js/src/ui/ParityBackground/parityBackground.js @@ -26,7 +26,12 @@ class ParityBackground extends Component { backgroundSeed: PropTypes.string, children: PropTypes.node, className: PropTypes.string, - onClick: PropTypes.func + onClick: PropTypes.func, + style: PropTypes.object + }; + + static defaultProps = { + style: {} }; state = { @@ -65,7 +70,11 @@ class ParityBackground extends Component { render () { const { children, className, onClick } = this.props; - const { style } = this.state; + + const style = { + ...this.state.style, + ...this.props.style + }; return (
", + "position": "top-right", "visible": true, "secure": true, "skipBuild": true diff --git a/js/src/views/Dapps/dappsStore.js b/js/src/views/Dapps/dappsStore.js index efbde9ef4..4fe86949e 100644 --- a/js/src/views/Dapps/dappsStore.js +++ b/js/src/views/Dapps/dappsStore.js @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +import EventEmitter from 'eventemitter3'; import { action, computed, observable, transaction } from 'mobx'; import store from 'store'; @@ -30,7 +31,7 @@ const BUILTIN_APPS_KEY = 'BUILTIN_APPS_KEY'; let instance = null; -export default class DappsStore { +export default class DappsStore extends EventEmitter { @observable apps = []; @observable displayApps = {}; @observable modalOpen = false; @@ -44,6 +45,8 @@ export default class DappsStore { _registryAppsIds = null; constructor (api) { + super(); + this._api = api; this.readDisplayApps(); @@ -51,6 +54,14 @@ export default class DappsStore { this.subscribeToChanges(); } + static get (api) { + if (!instance) { + instance = new DappsStore(api); + } + + return instance; + } + /** * Try to find the app from the local (local or builtin) * apps, else fetch from the node @@ -68,6 +79,10 @@ export default class DappsStore { } return this.fetchRegistryApp(dappReg, id, true); + }) + .then((app) => { + this.emit('loaded', app); + return app; }); } @@ -90,14 +105,6 @@ export default class DappsStore { .then(this.writeDisplayApps); } - static get (api) { - if (!instance) { - instance = new DappsStore(api); - } - - return instance; - } - subscribeToChanges () { const { dappReg } = Contracts.get(); diff --git a/js/src/views/ParityBar/parityBar.css b/js/src/views/ParityBar/parityBar.css index a955bd687..6db06c2f4 100644 --- a/js/src/views/ParityBar/parityBar.css +++ b/js/src/views/ParityBar/parityBar.css @@ -15,15 +15,6 @@ /* along with Parity. If not, see . */ -.bar, .expanded { - position: fixed; - bottom: 0; - right: 0; - font-size: 16px; - font-family: 'Roboto', sans-serif; - z-index: 10001; -} - .overlay { position: fixed; top: 0; @@ -32,6 +23,15 @@ left: 0; background: rgba(255, 255, 255, 0.5); z-index: 10000; + user-select: none; +} + +.bar, .expanded { + position: fixed; + font-size: 16px; + font-family: 'Roboto', sans-serif; + z-index: 10001; + user-select: none; } .bar { @@ -39,35 +39,57 @@ display: flex; flex-wrap: wrap; width: 100%; + top: 0; + left: 0; + + &.moving { + bottom: 0; + right: 0; + + &:hover { + cursor: move; + } + } +} + +.parityBg { + position: fixed; + + transition-property: left, top, right, bottom; + transition-duration: 0.25s; + transition-timing-function: ease; + + &.moving { + transition-duration: 0.05s; + transition-timing-function: ease-in-out; + } } .expanded { - right: 1em; border-radius: 4px 4px 0 0; display: flex; flex-direction: column; max-height: 50vh; -} -.expanded .content { - flex: 1; - overflow-y: auto; - overflow-x: hidden; - display: flex; - background: rgba(0, 0, 0, 0.8); - min-height: 16em; + .content { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + display: flex; + background: rgba(0, 0, 0, 0.8); + min-height: 16em; + } } .corner { - position: absolute; - bottom: 0; - right: 1em; border-radius: 4px 4px 0 0; } .cornercolor { background: rgba(0, 0, 0, 0.5); padding: 0.5em 1em; + display: flex; + align-items: center; } .link { @@ -76,12 +98,12 @@ outline: none !important; color: white !important; display: inline-block; -} -.link img, .link svg { - height: 24px !important; - width: 24px !important; - margin: 2px 0.5em 0 0; + img, svg { + height: 24px !important; + width: 24px !important; + margin: 2px 0.5em 0 0; + } } .link+.link { @@ -123,20 +145,20 @@ padding: 0.5em 1em; background: rgba(0, 0, 0, 0.25); margin-bottom: 0; + + &:after { + clear: both; + } } -.header:after { - clear: both; -} +.header, .corner { + button { + color: white !important; + } -.header button, -.corner button { - color: white !important; -} - -.header svg, -.coner svg { - fill: white !important; + svg { + fill: white !important; + } } .body { @@ -150,12 +172,12 @@ .actions { float: right; margin-top: -2px; -} -.actions div { - margin-left: 1em; - display: inline-block; - cursor: pointer; + div { + margin-left: 1em; + display: inline-block; + cursor: pointer; + } } .parityIcon, .signerIcon { @@ -164,3 +186,34 @@ vertical-align: middle; margin-left: 12px; } + +.moveIcon { + display: flex; + align-items: center; + + &:hover { + cursor: move; + } +} + +.dragButton { + width: 1em; + height: 1em; + margin-left: 0.5em; + + background-color: white; + opacity: 0.25; + border-radius: 50%; + + transition-property: opacity; + transition-duration: 0.1s; + transition-timing-function: ease-in-out; + + &:hover { + opacity: 0.5; + } + + &.moving { + opacity: 0.75; + } +} diff --git a/js/src/views/ParityBar/parityBar.js b/js/src/views/ParityBar/parityBar.js index af97e5655..d75890a90 100644 --- a/js/src/views/ParityBar/parityBar.js +++ b/js/src/views/ParityBar/parityBar.js @@ -15,25 +15,62 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; +import ReactDOM from 'react-dom'; import { Link } from 'react-router'; import { connect } from 'react-redux'; -import ActionFingerprint from 'material-ui/svg-icons/action/fingerprint'; -import ContentClear from 'material-ui/svg-icons/content/clear'; +import { throttle } from 'lodash'; +import store from 'store'; +import { CancelIcon, FingerprintIcon } from '~/ui/Icons'; import { Badge, Button, ContainerTitle, ParityBackground } from '~/ui'; import { Embedded as Signer } from '../Signer'; +import DappsStore from '~/views/Dapps/dappsStore'; import imagesEthcoreBlock from '../../../assets/images/parity-logo-white-no-text.svg'; import styles from './parityBar.css'; +const LS_STORE_KEY = '_parity::parityBar'; +const DEFAULT_POSITION = { right: '1em', bottom: 0 }; + class ParityBar extends Component { + app = null; + measures = null; + moving = false; + + static contextTypes = { + api: PropTypes.object.isRequired + }; + static propTypes = { - pending: PropTypes.array, - dapp: PropTypes.bool - } + dapp: PropTypes.bool, + pending: PropTypes.array + }; state = { - opened: false + moving: false, + opened: false, + position: DEFAULT_POSITION + }; + + constructor (props) { + super(props); + + this.debouncedMouseMove = throttle( + this._onMouseMove, + 40, + { leading: true, trailing: true } + ); + } + + componentWillMount () { + const { api } = this.context; + + // Hook to the dapp loaded event to position the + // Parity Bar accordingly + DappsStore.get(api).on('loaded', (app) => { + this.app = app; + this.loadPosition(); + }); } componentWillReceiveProps (nextProps) { @@ -52,11 +89,69 @@ class ParityBar extends Component { } render () { - const { opened } = this.state; + const { moving, opened, position } = this.state; - return opened + const content = opened ? this.renderExpanded() : this.renderBar(); + + const containerClassNames = opened + ? [ styles.overlay ] + : [ styles.bar ]; + + if (!opened && moving) { + containerClassNames.push(styles.moving); + } + + const parityBgClassName = opened + ? styles.expanded + : styles.corner; + + const parityBgClassNames = [ parityBgClassName, styles.parityBg ]; + + if (moving) { + parityBgClassNames.push(styles.moving); + } + + const parityBgStyle = { + ...position + }; + + // Open the Signer at one of the four corners + // of the screen + if (opened) { + // Set at top or bottom of the screen + if (position.top !== undefined) { + parityBgStyle.top = 0; + } else { + parityBgStyle.bottom = 0; + } + + // Set at left or right of the screen + if (position.left !== undefined) { + parityBgStyle.left = '1em'; + } else { + parityBgStyle.right = '1em'; + } + } + + return ( +
+ + { content } + +
+ ); } renderBar () { @@ -73,49 +168,59 @@ class ParityBar extends Component { /> ); + const dragButtonClasses = [ styles.dragButton ]; + + if (this.state.moving) { + dragButtonClasses.push(styles.moving); + } + return ( -
- -
- -
-
+
+ +
+
+
+
+
-
- +
+
- +
+
+ +
); } @@ -148,6 +253,178 @@ class ParityBar extends Component { return this.renderLabel('Signer', bubble); } + getHorizontal (x) { + const { page, button, container } = this.measures; + + const left = x - button.offset.left; + const centerX = left + container.width / 2; + + // left part of the screen + if (centerX < page.width / 2) { + return { left: Math.max(0, left) }; + } + + const right = page.width - x - button.offset.right; + return { right: Math.max(0, right) }; + } + + getVertical (y) { + const STICKY_SIZE = 75; + const { page, button, container } = this.measures; + + const top = y - button.offset.top; + const centerY = top + container.height / 2; + + // top part of the screen + if (centerY < page.height / 2) { + // Add Sticky edges + const stickyTop = top < STICKY_SIZE + ? 0 + : top; + + return { top: Math.max(0, stickyTop) }; + } + + const bottom = page.height - y - button.offset.bottom; + // Add Sticky edges + const stickyBottom = bottom < STICKY_SIZE + ? 0 + : bottom; + + return { bottom: Math.max(0, stickyBottom) }; + } + + getPosition (x, y) { + if (!this.moving || !this.measures) { + return {}; + } + + const horizontal = this.getHorizontal(x); + const vertical = this.getVertical(y); + + const position = { + ...horizontal, + ...vertical + }; + + return position; + } + + onMouseDown = (event) => { + const containerElt = ReactDOM.findDOMNode(this.refs.container); + const dragButtonElt = ReactDOM.findDOMNode(this.refs.dragButton); + + if (!containerElt || !dragButtonElt) { + console.warn(containerElt ? 'drag button' : 'container', 'not found...'); + return; + } + + const bodyRect = document.body.getBoundingClientRect(); + const containerRect = containerElt.getBoundingClientRect(); + const buttonRect = dragButtonElt.getBoundingClientRect(); + + const buttonOffset = { + top: (buttonRect.top + buttonRect.height / 2) - containerRect.top, + left: (buttonRect.left + buttonRect.width / 2) - containerRect.left + }; + + buttonOffset.bottom = containerRect.height - buttonOffset.top; + buttonOffset.right = containerRect.width - buttonOffset.left; + + const button = { + offset: buttonOffset, + height: buttonRect.height, + width: buttonRect.width + }; + + const container = { + height: containerRect.height, + width: containerRect.width + }; + + const page = { + height: bodyRect.height, + width: bodyRect.width + }; + + this.moving = true; + this.measures = { + button, + container, + page + }; + + this.setState({ moving: true }); + } + + onMouseEnter = (event) => { + if (!this.moving) { + return; + } + + const { buttons } = event; + + // If no left-click, stop move + if (buttons !== 1) { + this.onMouseUp(event); + } + } + + onMouseLeave = (event) => { + if (!this.moving) { + return; + } + + event.stopPropagation(); + event.preventDefault(); + } + + onMouseMove = (event) => { + const { pageX, pageY } = event; + // this._onMouseMove({ pageX, pageY }); + this.debouncedMouseMove({ pageX, pageY }); + + event.stopPropagation(); + event.preventDefault(); + } + + _onMouseMove = (event) => { + if (!this.moving) { + return; + } + + const { pageX, pageY } = event; + const position = this.getPosition(pageX, pageY); + this.setState({ position }); + } + + onMouseUp = (event) => { + if (!this.moving) { + return; + } + + const { pageX, pageY } = event; + const position = this.getPosition(pageX, pageY); + + // Stick to bottom or top + if (position.top !== undefined) { + position.top = 0; + } else { + position.bottom = 0; + } + + // Stick to bottom or top + if (position.left !== undefined) { + position.left = '1em'; + } else { + position.right = '1em'; + } + + this.moving = false; + this.setState({ moving: false, position }); + this.savePosition(position); + } + toggleDisplay = () => { const { opened } = this.state; @@ -155,6 +432,57 @@ class ParityBar extends Component { opened: !opened }); } + + get config () { + let config; + + try { + config = JSON.parse(store.get(LS_STORE_KEY)); + } catch (error) { + config = {}; + } + + return config; + } + + loadPosition (props = this.props) { + const { app, config } = this; + + if (!app) { + return this.setState({ position: DEFAULT_POSITION }); + } + + if (config[app.id]) { + return this.setState({ position: config[app.id] }); + } + + const position = this.stringToPosition(app.position); + this.setState({ position }); + } + + savePosition (position) { + const { app, config } = this; + config[app.id] = position; + + store.set(LS_STORE_KEY, JSON.stringify(config)); + } + + stringToPosition (value) { + switch (value) { + case 'top-left': + return { top: 0, left: '1em' }; + + case 'top-right': + return { top: 0, right: '1em' }; + + case 'bottom-left': + return { bottom: 0, left: '1em' }; + + case 'bottom-right': + default: + return DEFAULT_POSITION; + } + } } function mapStateToProps (state) {