summaryrefslogtreecommitdiffhomepage
path: root/app
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@codeispoetry.ru>2017-02-17 15:17:20 +0000
committerAndrej Mihajlov <and@codeispoetry.ru>2017-02-17 15:17:20 +0000
commit00b82cb3aa904b34d7000854bcecd3b72e012c8e (patch)
treeac314579a84d6c5b6fbbda8f0a0984f023364870 /app
parente88018b890c8ddf62895101439d57ca206d9379b (diff)
downloadmullvadvpn-00b82cb3aa904b34d7000854bcecd3b72e012c8e.tar.xz
mullvadvpn-00b82cb3aa904b34d7000854bcecd3b72e012c8e.zip
Add switch control and hook up settings reducer & actions
Diffstat (limited to 'app')
-rw-r--r--app/actions/connect.js2
-rw-r--r--app/actions/settings.js5
-rw-r--r--app/assets/css/style.css1
-rw-r--r--app/assets/css/uiswitch.css135
-rw-r--r--app/components/Settings.css2
-rw-r--r--app/components/Settings.js11
-rw-r--r--app/components/Switch.css44
-rw-r--r--app/components/Switch.js107
-rw-r--r--app/containers/SettingsPage.js5
-rw-r--r--app/reducers/connect.js4
-rw-r--r--app/reducers/settings.js13
-rw-r--r--app/store.js8
12 files changed, 330 insertions, 7 deletions
diff --git a/app/actions/connect.js b/app/actions/connect.js
index ff8b4c5632..518822a5b4 100644
--- a/app/actions/connect.js
+++ b/app/actions/connect.js
@@ -1 +1,3 @@
+import { createAction } from 'redux-actions';
+
export default {};
diff --git a/app/actions/settings.js b/app/actions/settings.js
new file mode 100644
index 0000000000..7a482a578f
--- /dev/null
+++ b/app/actions/settings.js
@@ -0,0 +1,5 @@
+import { createAction } from 'redux-actions';
+
+const updateSettings = createAction('SETTINGS_UPDATE');
+
+export default { updateSettings };
diff --git a/app/assets/css/style.css b/app/assets/css/style.css
index e7cc339332..7d16da3fe3 100644
--- a/app/assets/css/style.css
+++ b/app/assets/css/style.css
@@ -6,3 +6,4 @@
@import '../../components/Settings.css';
@import '../../components/HeaderBar.css';
@import '../../components/Layout.css';
+@import '../../components/Switch.css';
diff --git a/app/assets/css/uiswitch.css b/app/assets/css/uiswitch.css
new file mode 100644
index 0000000000..e8c037bf1e
--- /dev/null
+++ b/app/assets/css/uiswitch.css
@@ -0,0 +1,135 @@
+/* https://github.com/fnky/css3-uiswitch */
+.uiswitch {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ -o-appearance: none;
+ appearance: none;
+ height: 31px;
+ width: 51px;
+ position: relative;
+ border-radius: 16px;
+ cursor: pointer;
+ outline: 0;
+ z-index: 0;
+ margin: 0;
+ padding: 0;
+ border: none;
+ background-color: #e5e5e5;
+ -webkit-transition-duration: 600ms;
+ -moz-transition-duration: 600ms;
+ transition-duration: 600ms;
+ -webkit-transition-timing-function: ease-in-out;
+ -moz-transition-timing-function: ease-in-out;
+ transition-timing-function: ease-in-out;
+ -webkit-touch-callout: none;
+ -webkit-text-size-adjust: none;
+ -webkit-tap-highlight-color: transparent;
+ -webkit-user-select: none; }
+ .uiswitch::before {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ height: 27px;
+ width: 47px;
+ content: ' ';
+ position: absolute;
+ left: 2px;
+ top: 2px;
+ background-color: white;
+ border-radius: 16px;
+ z-index: 1;
+ -webkit-transition-duration: 300ms;
+ -moz-transition-duration: 300ms;
+ transition-duration: 300ms;
+ -webkit-transform: scale(1);
+ -moz-transform: scale(1);
+ -ms-transform: scale(1);
+ -o-transform: scale(1);
+ transform: scale(1); }
+ .uiswitch::after {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ height: 27px;
+ width: 27px;
+ content: ' ';
+ position: absolute;
+ border-radius: 27px;
+ background: white;
+ z-index: 2;
+ top: 2px;
+ left: 2px;
+ box-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0.25), 0px 4px 11px 0px rgba(0, 0, 0, 0.08), -1px 3px 3px 0px rgba(0, 0, 0, 0.14);
+ -webkit-transition: -webkit-transform 300ms, width 280ms;
+ -moz-transition: -moz-transform 300ms, width 280ms;
+ transition: transform 300ms, width 280ms;
+ -webkit-transform: translate3d(0, 0, 0);
+ -moz-transform: translate3d(0, 0, 0);
+ -ms-transform: translate3d(0, 0, 0);
+ -o-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ -webkit-transition-timing-function: cubic-bezier(0.42, 0.8, 0.58, 1.2);
+ -moz-transition-timing-function: cubic-bezier(0.42, 0.8, 0.58, 1.2);
+ transition-timing-function: cubic-bezier(0.42, 0.8, 0.58, 1.2); }
+ .uiswitch:checked {
+ background-color: #4cd964;
+ background-image: -webkit-linear-gradient(-90deg, #4cd964 0%, #4dd865 100%);
+ background-image: linear-gradient(-180deg,#4cd964 0%, #4dd865 100%); }
+ .uiswitch:checked::after {
+ -webkit-transform: translate3d(16px, 0, 0);
+ -moz-transform: translate3d(16px, 0, 0);
+ -ms-transform: translate3d(16px, 0, 0);
+ -o-transform: translate3d(16px, 0, 0);
+ transform: translate3d(16px, 0, 0);
+ right: 18px;
+ left: inherit; }
+ .uiswitch:active::after {
+ width: 35px; }
+ .uiswitch:checked::before, .uiswitch:active::before {
+ -webkit-transform: scale(0);
+ -moz-transform: scale(0);
+ -ms-transform: scale(0);
+ -o-transform: scale(0);
+ transform: scale(0); }
+ .uiswitch:disabled {
+ opacity: 0.5;
+ cursor: default;
+ -webkit-transition: none;
+ -moz-transition: none;
+ transition: none; }
+ .uiswitch:disabled:active::before, .uiswitch:disabled:active::after, .uiswitch:disabled:checked:active::before, .uiswitch:disabled:checked::before {
+ width: 27px;
+ -webkit-transition: none;
+ -moz-transition: none;
+ transition: none; }
+ .uiswitch:disabled:active::before {
+ height: 27px;
+ width: 41px;
+ -webkit-transform: translate3d(6px, 0, 0);
+ -moz-transform: translate3d(6px, 0, 0);
+ -ms-transform: translate3d(6px, 0, 0);
+ -o-transform: translate3d(6px, 0, 0);
+ transform: translate3d(6px, 0, 0); }
+ .uiswitch:disabled:checked:active::before {
+ height: 27px;
+ width: 27px;
+ -webkit-transform: scale(0);
+ -moz-transform: scale(0);
+ -ms-transform: scale(0);
+ -o-transform: scale(0);
+ transform: scale(0); }
+
+.uiswitch {
+ background-color: #e5e5e5; }
+ .uiswitch::before {
+ background-color: white; }
+ .uiswitch::after {
+ background: white; }
+ .uiswitch:checked {
+ background-color: #4cd964;
+ background-image: -webkit-linear-gradient(-90deg, #4cd964 0%, #4dd865 100%);
+ background-image: linear-gradient(-180deg,#4cd964 0%, #4dd865 100%); } \ No newline at end of file
diff --git a/app/components/Settings.css b/app/components/Settings.css
index 30186446e4..350ae406a5 100644
--- a/app/components/Settings.css
+++ b/app/components/Settings.css
@@ -101,7 +101,7 @@
}
.settings__cell-value {
-
+ flex: 0 0 auto;
}
.settings__cell-footer {
diff --git a/app/components/Settings.js b/app/components/Settings.js
index 5b36d8a64c..8089482f02 100644
--- a/app/components/Settings.js
+++ b/app/components/Settings.js
@@ -1,16 +1,23 @@
import React, { Component, PropTypes } from 'react';
import { Layout, Container, Header } from './Layout';
+import Switch from './Switch';
export default class Settings extends Component {
static propTypes = {
- logout: PropTypes.func.isRequired
+ logout: PropTypes.func.isRequired,
+ updateSettings: PropTypes.func.isRequired
}
onClose() {
this.props.router.push('/connect');
}
+ handleAutoSecure(isOn) {
+ console.log('autoSecure: ' + isOn);
+ this.props.updateSettings({ autoSecure: isOn });
+ }
+
render() {
return (
<Layout>
@@ -31,7 +38,7 @@ export default class Settings extends Component {
<div className="settings__cell">
<div className="settings__cell-label">Auto-secure</div>
<div className="settings__cell-value">
- <input type="checkbox" className="settings__switch" />
+ <Switch onChange={ ::this.handleAutoSecure } isOn={ this.props.settings.autoSecure } />
</div>
</div>
<div className="settings__cell-footer">
diff --git a/app/components/Switch.css b/app/components/Switch.css
new file mode 100644
index 0000000000..961312c103
--- /dev/null
+++ b/app/components/Switch.css
@@ -0,0 +1,44 @@
+.switch {
+ display: block;
+ position: relative;
+ -webkit-appearance: none;
+ border-radius: 16px;
+ width: 50px;
+ height: 30px;
+ border: 2px solid white;
+ background-color: transparent;
+ transition: 300ms ease-in-out all;
+}
+
+.switch:checked {
+ text-align: right;
+}
+
+.switch::after {
+ position: absolute;
+ left: 1px;
+ top: 1px;
+ display: block;
+ content: '';
+ width: 24px;
+ height: 24px;
+ border-radius: 24px;
+ background-color: #D0021B;
+ transition: 300ms ease-in-out all;
+ transform: translate3d(0, 0, 0);
+}
+
+.switch:active::after {
+ width: 28px;
+}
+
+.switch:active:checked::after {
+ transform: translate3d(0, 0, 0);
+ left: 17px;
+}
+
+.switch:checked::after {
+ background-color: #44AD4D;
+ transform: translate3d(0, 0, 0);
+ left: 21px;
+} \ No newline at end of file
diff --git a/app/components/Switch.js b/app/components/Switch.js
new file mode 100644
index 0000000000..16b64b2211
--- /dev/null
+++ b/app/components/Switch.js
@@ -0,0 +1,107 @@
+import React, { Component, PropTypes } from 'react';
+
+export default class Switch extends Component {
+
+ static propTypes = {
+ isOn: PropTypes.bool,
+ onChange: PropTypes.func
+ }
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isTracking: false,
+ ignoreChange: false,
+ initialPos: null,
+ startTime: null,
+ target: null
+ };
+ }
+
+ handleMouseDown(e) {
+ const { pageX: x, pageY: y } = e;
+ this.setState({
+ isTracking: true,
+ initialPos: { x, y },
+ startTime: e.timeStamp
+ });
+ }
+
+ handleMouseMove(e) {
+ if(!this.state.isTracking) {
+ return;
+ }
+
+ const thresholdX = 10, thresholdY = 50;
+ const { x: x0, y: y0 } = this.state.initialPos;
+ const { pageX: x, pageY: y } = e;
+
+ const dx = Math.abs(x0 - x);
+ const dy = Math.abs(y0 - y);
+
+ if(dx < thresholdX || dy > thresholdY) {
+ return;
+ }
+
+ const isOn = !!this.props.isOn;
+ let nextOn = isOn;
+
+ if(x < x0 && isOn) {
+ nextOn = false;
+ } else if(x > x0 && !isOn) {
+ nextOn = true;
+ }
+
+ if(isOn !== nextOn) {
+ this.setState({ initialPos: { x, y } });
+ this.refs.input.checked = nextOn;
+ this.notify(nextOn);
+ this.setState({ ignoreChange: true });
+ }
+ }
+
+ handleMouseUp() {
+ if(this.state.isTracking) {
+ this.setState({ isTracking: false, initialPos: null });
+ console.log('mouseup');
+ }
+ }
+
+ handleChange(e) {
+ console.log('ONCHANGE ' + e.target.checked);
+ const delta = e.timeStamp - this.state.startTime;
+ const threshold = 1000;
+
+ if(this.state.ignoreChange) {
+ e.preventDefault();
+ this.setState({ ignoreChange: false });
+ } else if(delta > threshold) {
+ e.preventDefault();
+ } else {
+ this.notify(e.target.checked);
+ }
+ }
+
+ notify(isOn) {
+ if(this.props.onChange) {
+ this.props.onChange(isOn);
+ }
+ }
+
+ componentDidMount() {
+ document.addEventListener('mousemove', ::this.handleMouseMove);
+ document.addEventListener('mouseup', ::this.handleMouseUp);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('mousemove', ::this.handleMouseMove);
+ document.removeEventListener('mouseup', ::this.handleMouseUp);
+ }
+
+ render() {
+ return (
+ <input type="checkbox" ref="input" className="switch" checked={ this.props.isOn }
+ onMouseDown={ ::this.handleMouseDown } onChange={ ::this.handleChange } />
+ );
+ }
+} \ No newline at end of file
diff --git a/app/containers/SettingsPage.js b/app/containers/SettingsPage.js
index eb70ad00b8..5489eb5238 100644
--- a/app/containers/SettingsPage.js
+++ b/app/containers/SettingsPage.js
@@ -2,6 +2,7 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Settings from '../components/Settings';
import userActions from '../actions/user';
+import settingsActions from '../actions/settings';
const mapStateToProps = (state) => {
return state;
@@ -9,10 +10,12 @@ const mapStateToProps = (state) => {
const mapDispatchToProps = (dispatch, props) => {
const user = bindActionCreators(userActions, dispatch);
+ const settings = bindActionCreators(settingsActions, dispatch);
return {
logout: () => {
return user.logout(props.backend);
- }
+ },
+ updateSettings: settings.updateSettings
};
};
diff --git a/app/reducers/connect.js b/app/reducers/connect.js
index 1340fdd399..97e47c5e72 100644
--- a/app/reducers/connect.js
+++ b/app/reducers/connect.js
@@ -4,6 +4,4 @@ import actions from '../actions/connect';
const initialState = {};
-export default handleActions({
-
-}, initialState);
+export default handleActions({ test: (state) => { return state; } }, initialState);
diff --git a/app/reducers/settings.js b/app/reducers/settings.js
new file mode 100644
index 0000000000..f4f44119ad
--- /dev/null
+++ b/app/reducers/settings.js
@@ -0,0 +1,13 @@
+import { handleActions } from 'redux-actions';
+
+import actions from '../actions/settings';
+
+const initialState = {
+ autoSecure: false
+};
+
+export default handleActions({
+ [actions.updateSettings]: (state, action) => {
+ return { ...state, ...action.payload };
+ }
+}, initialState);
diff --git a/app/store.js b/app/store.js
index 55a3df85f8..852712e345 100644
--- a/app/store.js
+++ b/app/store.js
@@ -5,18 +5,26 @@ import persistState from 'redux-localstorage';
import thunk from 'redux-thunk';
import user from './reducers/user';
+import connect from './reducers/connect';
+import settings from './reducers/settings';
import userActions from './actions/user';
+import connectActions from './actions/connect';
+import settingsActions from './actions/settings';
const router = routerMiddleware(hashHistory);
const actionCreators = {
...userActions,
+ ...connectActions,
+ ...settingsActions,
pushRoute: (route) => push(route),
replaceRoute: (route) => replace(route),
};
const reducers = {
user,
+ connect,
+ settings,
routing
};