diff --git a/js/src/ui/Icons/index.js b/js/src/ui/Icons/index.js
index a35bbb61040..fc3b67391e9 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 6e554b84679..cab3f31d3f2 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 efbde9ef430..4fe86949ed7 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 a955bd6870f..6db06c2f451 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;
-}
-.header:after {
- clear: both;
+ &:after {
+ clear: both;
+ }
}
-.header button,
-.corner button {
- color: white !important;
-}
+.header, .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 af97e565517..d75890a9040 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 (
+
+ );
}
renderBar () {
@@ -73,49 +168,59 @@ class ParityBar extends Component {
/>
);
+ const dragButtonClasses = [ styles.dragButton ];
+
+ if (this.state.moving) {
+ dragButtonClasses.push(styles.moving);
+ }
+
return (
-
-
-
-
-
-
- }
- label={ this.renderSignerLabel() }
- onClick={ this.toggleDisplay }
- />
-
-
+
+
+
+
+
}
+ label={ this.renderSignerLabel() }
+ onClick={ this.toggleDisplay }
+ />
+
+
);
}
renderExpanded () {
return (
-
-
-
-
-
-
-
- }
- label='Close'
- onClick={ this.toggleDisplay }
- />
-
+
+
+
+
-
-
+
+ }
+ label='Close'
+ onClick={ this.toggleDisplay }
+ />
-
+
+
+
+
);
}
@@ -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) {