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
This commit is contained in:
Nicolas Gotchac 2017-01-21 14:44:14 +01:00 committed by Gav Wood
parent ebe9072836
commit 6b52ed4dfa
6 changed files with 497 additions and 95 deletions

View File

@ -25,8 +25,10 @@ import DashboardIcon from 'material-ui/svg-icons/action/dashboard';
import DeleteIcon from 'material-ui/svg-icons/action/delete'; import DeleteIcon from 'material-ui/svg-icons/action/delete';
import DoneIcon from 'material-ui/svg-icons/action/done-all'; import DoneIcon from 'material-ui/svg-icons/action/done-all';
import EditIcon from 'material-ui/svg-icons/content/create'; 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 LinkIcon from 'material-ui/svg-icons/content/link';
import LockedIcon from 'material-ui/svg-icons/action/lock'; 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 NextIcon from 'material-ui/svg-icons/navigation/arrow-forward';
import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back'; import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back';
import SaveIcon from 'material-ui/svg-icons/content/save'; import SaveIcon from 'material-ui/svg-icons/content/save';
@ -48,8 +50,10 @@ export {
DeleteIcon, DeleteIcon,
DoneIcon, DoneIcon,
EditIcon, EditIcon,
FingerprintIcon,
LinkIcon, LinkIcon,
LockedIcon, LockedIcon,
MoveIcon,
NextIcon, NextIcon,
PrevIcon, PrevIcon,
SaveIcon, SaveIcon,

View File

@ -26,7 +26,12 @@ class ParityBackground extends Component {
backgroundSeed: PropTypes.string, backgroundSeed: PropTypes.string,
children: PropTypes.node, children: PropTypes.node,
className: PropTypes.string, className: PropTypes.string,
onClick: PropTypes.func onClick: PropTypes.func,
style: PropTypes.object
};
static defaultProps = {
style: {}
}; };
state = { state = {
@ -65,7 +70,11 @@ class ParityBackground extends Component {
render () { render () {
const { children, className, onClick } = this.props; const { children, className, onClick } = this.props;
const { style } = this.state;
const style = {
...this.state.style,
...this.props.style
};
return ( return (
<div <div

View File

@ -81,6 +81,7 @@
"description": "A Javascript development console complete with web3 and parity objects.", "description": "A Javascript development console complete with web3 and parity objects.",
"version": "0.3", "version": "0.3",
"author": "Gav Wood <gavin@ethcore.io>", "author": "Gav Wood <gavin@ethcore.io>",
"position": "top-right",
"visible": true, "visible": true,
"secure": true, "secure": true,
"skipBuild": true "skipBuild": true

View File

@ -14,6 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import EventEmitter from 'eventemitter3';
import { action, computed, observable, transaction } from 'mobx'; import { action, computed, observable, transaction } from 'mobx';
import store from 'store'; import store from 'store';
@ -30,7 +31,7 @@ const BUILTIN_APPS_KEY = 'BUILTIN_APPS_KEY';
let instance = null; let instance = null;
export default class DappsStore { export default class DappsStore extends EventEmitter {
@observable apps = []; @observable apps = [];
@observable displayApps = {}; @observable displayApps = {};
@observable modalOpen = false; @observable modalOpen = false;
@ -44,6 +45,8 @@ export default class DappsStore {
_registryAppsIds = null; _registryAppsIds = null;
constructor (api) { constructor (api) {
super();
this._api = api; this._api = api;
this.readDisplayApps(); this.readDisplayApps();
@ -51,6 +54,14 @@ export default class DappsStore {
this.subscribeToChanges(); this.subscribeToChanges();
} }
static get (api) {
if (!instance) {
instance = new DappsStore(api);
}
return instance;
}
/** /**
* Try to find the app from the local (local or builtin) * Try to find the app from the local (local or builtin)
* apps, else fetch from the node * apps, else fetch from the node
@ -68,6 +79,10 @@ export default class DappsStore {
} }
return this.fetchRegistryApp(dappReg, id, true); 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); .then(this.writeDisplayApps);
} }
static get (api) {
if (!instance) {
instance = new DappsStore(api);
}
return instance;
}
subscribeToChanges () { subscribeToChanges () {
const { dappReg } = Contracts.get(); const { dappReg } = Contracts.get();

View File

@ -15,15 +15,6 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
.bar, .expanded {
position: fixed;
bottom: 0;
right: 0;
font-size: 16px;
font-family: 'Roboto', sans-serif;
z-index: 10001;
}
.overlay { .overlay {
position: fixed; position: fixed;
top: 0; top: 0;
@ -32,6 +23,15 @@
left: 0; left: 0;
background: rgba(255, 255, 255, 0.5); background: rgba(255, 255, 255, 0.5);
z-index: 10000; 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 { .bar {
@ -39,35 +39,57 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
width: 100%; 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 { .expanded {
right: 1em;
border-radius: 4px 4px 0 0; border-radius: 4px 4px 0 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-height: 50vh; max-height: 50vh;
}
.expanded .content { .content {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
display: flex; display: flex;
background: rgba(0, 0, 0, 0.8); background: rgba(0, 0, 0, 0.8);
min-height: 16em; min-height: 16em;
}
} }
.corner { .corner {
position: absolute;
bottom: 0;
right: 1em;
border-radius: 4px 4px 0 0; border-radius: 4px 4px 0 0;
} }
.cornercolor { .cornercolor {
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
padding: 0.5em 1em; padding: 0.5em 1em;
display: flex;
align-items: center;
} }
.link { .link {
@ -76,12 +98,12 @@
outline: none !important; outline: none !important;
color: white !important; color: white !important;
display: inline-block; display: inline-block;
}
.link img, .link svg { img, svg {
height: 24px !important; height: 24px !important;
width: 24px !important; width: 24px !important;
margin: 2px 0.5em 0 0; margin: 2px 0.5em 0 0;
}
} }
.link+.link { .link+.link {
@ -123,20 +145,20 @@
padding: 0.5em 1em; padding: 0.5em 1em;
background: rgba(0, 0, 0, 0.25); background: rgba(0, 0, 0, 0.25);
margin-bottom: 0; margin-bottom: 0;
&:after {
clear: both;
}
} }
.header:after { .header, .corner {
clear: both; button {
} color: white !important;
}
.header button, svg {
.corner button { fill: white !important;
color: white !important; }
}
.header svg,
.coner svg {
fill: white !important;
} }
.body { .body {
@ -150,12 +172,12 @@
.actions { .actions {
float: right; float: right;
margin-top: -2px; margin-top: -2px;
}
.actions div { div {
margin-left: 1em; margin-left: 1em;
display: inline-block; display: inline-block;
cursor: pointer; cursor: pointer;
}
} }
.parityIcon, .signerIcon { .parityIcon, .signerIcon {
@ -164,3 +186,34 @@
vertical-align: middle; vertical-align: middle;
margin-left: 12px; 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;
}
}

View File

@ -15,25 +15,62 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ActionFingerprint from 'material-ui/svg-icons/action/fingerprint'; import { throttle } from 'lodash';
import ContentClear from 'material-ui/svg-icons/content/clear'; import store from 'store';
import { CancelIcon, FingerprintIcon } from '~/ui/Icons';
import { Badge, Button, ContainerTitle, ParityBackground } from '~/ui'; import { Badge, Button, ContainerTitle, ParityBackground } from '~/ui';
import { Embedded as Signer } from '../Signer'; import { Embedded as Signer } from '../Signer';
import DappsStore from '~/views/Dapps/dappsStore';
import imagesEthcoreBlock from '../../../assets/images/parity-logo-white-no-text.svg'; import imagesEthcoreBlock from '../../../assets/images/parity-logo-white-no-text.svg';
import styles from './parityBar.css'; import styles from './parityBar.css';
const LS_STORE_KEY = '_parity::parityBar';
const DEFAULT_POSITION = { right: '1em', bottom: 0 };
class ParityBar extends Component { class ParityBar extends Component {
app = null;
measures = null;
moving = false;
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = { static propTypes = {
pending: PropTypes.array, dapp: PropTypes.bool,
dapp: PropTypes.bool pending: PropTypes.array
} };
state = { 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) { componentWillReceiveProps (nextProps) {
@ -52,11 +89,69 @@ class ParityBar extends Component {
} }
render () { render () {
const { opened } = this.state; const { moving, opened, position } = this.state;
return opened const content = opened
? this.renderExpanded() ? this.renderExpanded()
: this.renderBar(); : 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 (
<div
className={ containerClassNames.join(' ') }
onMouseEnter={ this.onMouseEnter }
onMouseLeave={ this.onMouseLeave }
onMouseMove={ this.onMouseMove }
onMouseUp={ this.onMouseUp }
>
<ParityBackground
className={ parityBgClassNames.join(' ') }
ref='container'
style={ parityBgStyle }
>
{ content }
</ParityBackground>
</div>
);
} }
renderBar () { renderBar () {
@ -73,49 +168,59 @@ class ParityBar extends Component {
/> />
); );
const dragButtonClasses = [ styles.dragButton ];
if (this.state.moving) {
dragButtonClasses.push(styles.moving);
}
return ( return (
<div className={ styles.bar }> <div className={ styles.cornercolor }>
<ParityBackground className={ styles.corner }> <Link to='/apps'>
<div className={ styles.cornercolor }> <Button
<Link to='/apps'> className={ styles.parityButton }
<Button icon={ parityIcon }
className={ styles.parityButton } label={ this.renderLabel('Parity') }
icon={ parityIcon } />
label={ this.renderLabel('Parity') } </Link>
/> <Button
</Link> className={ styles.button }
<Button icon={ <FingerprintIcon /> }
className={ styles.button } label={ this.renderSignerLabel() }
icon={ <ActionFingerprint /> } onClick={ this.toggleDisplay }
label={ this.renderSignerLabel() } />
onClick={ this.toggleDisplay }
/> <div
</div> className={ styles.moveIcon }
</ParityBackground> onMouseDown={ this.onMouseDown }
>
<div
className={ dragButtonClasses.join(' ') }
ref='dragButton'
/>
</div>
</div> </div>
); );
} }
renderExpanded () { renderExpanded () {
return ( return (
<div className={ styles.overlay }> <div>
<ParityBackground className={ styles.expanded }> <div className={ styles.header }>
<div className={ styles.header }> <div className={ styles.title }>
<div className={ styles.title }> <ContainerTitle title='Parity Signer: Pending' />
<ContainerTitle title='Parity Signer: Pending' />
</div>
<div className={ styles.actions }>
<Button
icon={ <ContentClear /> }
label='Close'
onClick={ this.toggleDisplay }
/>
</div>
</div> </div>
<div className={ styles.content }> <div className={ styles.actions }>
<Signer /> <Button
icon={ <CancelIcon /> }
label='Close'
onClick={ this.toggleDisplay }
/>
</div> </div>
</ParityBackground> </div>
<div className={ styles.content }>
<Signer />
</div>
</div> </div>
); );
} }
@ -148,6 +253,178 @@ class ParityBar extends Component {
return this.renderLabel('Signer', bubble); 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 = () => { toggleDisplay = () => {
const { opened } = this.state; const { opened } = this.state;
@ -155,6 +432,57 @@ class ParityBar extends Component {
opened: !opened 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) { function mapStateToProps (state) {