diff options
| author | Andrej Mihajlov <and@codeispoetry.ru> | 2017-02-17 15:17:20 +0000 |
|---|---|---|
| committer | Andrej Mihajlov <and@codeispoetry.ru> | 2017-02-17 15:17:20 +0000 |
| commit | 00b82cb3aa904b34d7000854bcecd3b72e012c8e (patch) | |
| tree | ac314579a84d6c5b6fbbda8f0a0984f023364870 | |
| parent | e88018b890c8ddf62895101439d57ca206d9379b (diff) | |
| download | mullvadvpn-00b82cb3aa904b34d7000854bcecd3b72e012c8e.tar.xz mullvadvpn-00b82cb3aa904b34d7000854bcecd3b72e012c8e.zip | |
Add switch control and hook up settings reducer & actions
| -rw-r--r-- | app/actions/connect.js | 2 | ||||
| -rw-r--r-- | app/actions/settings.js | 5 | ||||
| -rw-r--r-- | app/assets/css/style.css | 1 | ||||
| -rw-r--r-- | app/assets/css/uiswitch.css | 135 | ||||
| -rw-r--r-- | app/components/Settings.css | 2 | ||||
| -rw-r--r-- | app/components/Settings.js | 11 | ||||
| -rw-r--r-- | app/components/Switch.css | 44 | ||||
| -rw-r--r-- | app/components/Switch.js | 107 | ||||
| -rw-r--r-- | app/containers/SettingsPage.js | 5 | ||||
| -rw-r--r-- | app/reducers/connect.js | 4 | ||||
| -rw-r--r-- | app/reducers/settings.js | 13 | ||||
| -rw-r--r-- | app/store.js | 8 |
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 }; |
