diff options
| author | Andrei Mihailov <and.mikhaylov@gmail.com> | 2017-02-16 12:18:52 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2017-02-16 12:18:52 +0000 |
| commit | 4aaf7e0ddbbdf9a5e03fdbb20ecabe4866de96c3 (patch) | |
| tree | 8f7dfc5545c484472d3dc5138cddf6a810fa0558 /app | |
| parent | 5ba4a72e546747167e7ae815724ee2dd4b2ef8fb (diff) | |
| download | mullvadvpn-4aaf7e0ddbbdf9a5e03fdbb20ecabe4866de96c3.tar.xz mullvadvpn-4aaf7e0ddbbdf9a5e03fdbb20ecabe4866de96c3.zip | |
Feature/menubar popup window (#1)
Add menubar and tests
Diffstat (limited to 'app')
| -rw-r--r-- | app/actions/connect.js | 2 | ||||
| -rw-r--r-- | app/app.js | 31 | ||||
| -rw-r--r-- | app/assets/css/style.css | 2 | ||||
| -rw-r--r-- | app/assets/images/app-triangle-error.svg | 5 | ||||
| -rw-r--r-- | app/assets/images/app-triangle-success.svg | 5 | ||||
| -rw-r--r-- | app/assets/images/app-triangle.svg | 5 | ||||
| -rw-r--r-- | app/assets/images/trayIconTemplate.png (renamed from app/assets/images/trayicon.png) | bin | 950 -> 950 bytes | |||
| -rw-r--r-- | app/assets/images/trayIconTemplate@2x.png (renamed from app/assets/images/trayicon@2x.png) | bin | 1316 -> 1316 bytes | |||
| -rw-r--r-- | app/components/Connect.js | 9 | ||||
| -rw-r--r-- | app/components/HeaderBar.css | 29 | ||||
| -rw-r--r-- | app/components/HeaderBar.js | 2 | ||||
| -rw-r--r-- | app/components/Layout.css | 2 | ||||
| -rw-r--r-- | app/components/Layout.js | 4 | ||||
| -rw-r--r-- | app/components/Tray.js | 39 | ||||
| -rw-r--r-- | app/containers/ConnectPage.js | 11 | ||||
| -rw-r--r-- | app/containers/Tray.js | 14 | ||||
| -rw-r--r-- | app/lib/components/TrayMenu.js | 120 | ||||
| -rw-r--r-- | app/main.js | 180 | ||||
| -rw-r--r-- | app/store.js | 5 |
19 files changed, 192 insertions, 273 deletions
diff --git a/app/actions/connect.js b/app/actions/connect.js index b1c6ea436a..ff8b4c5632 100644 --- a/app/actions/connect.js +++ b/app/actions/connect.js @@ -1 +1 @@ -export default {} +export default {}; diff --git a/app/app.js b/app/app.js index 6ac3ce492f..8789608887 100644 --- a/app/app.js +++ b/app/app.js @@ -3,21 +3,27 @@ import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import { Router, hashHistory } from 'react-router'; import { syncHistoryWithStore } from 'react-router-redux'; -import { remote, webFrame } from 'electron'; -import path from 'path'; +import { webFrame } from 'electron'; import routes from './routes'; import configureStore from './store'; -import Tray from './containers/Tray'; import Backend from './lib/backend'; const backend = new Backend(); -const iconPath = path.join(__dirname, 'assets/images/trayicon.png'); -const tray = new remote.Tray(iconPath); - const initialState = {}; const store = configureStore(initialState); -const routerHistory = syncHistoryWithStore(hashHistory, store); + +// desperately trying to fix https://github.com/reactjs/react-router-redux/issues/534 +hashHistory.replace('/'); + +// see https://github.com/reactjs/react-router-redux/issues/534 +const recentLocation = (store.getState().routing || {}).locationBeforeTransitions; +const routerHistory = syncHistoryWithStore(hashHistory, store, { adjustUrlOnReplay: true }); + +if(recentLocation && recentLocation.pathname) { + routerHistory.replace(recentLocation.pathname); +} + const rootElement = document.querySelector(document.currentScript.getAttribute('data-container')); // disable smart pinch. @@ -32,13 +38,8 @@ const createElement = (Component, props) => { }; ReactDOM.render( - <div> - <Provider store={ store }> - <Router history={ routerHistory } routes={ routes } createElement={ createElement } /> - </Provider> - <Provider store={ store }> - <Tray handle={ tray } backend={ backend } /> - </Provider> - </div>, + <Provider store={ store }> + <Router history={ routerHistory } routes={ routes } createElement={ createElement } /> + </Provider>, rootElement ); diff --git a/app/assets/css/style.css b/app/assets/css/style.css index fa883fee98..3b8706f63e 100644 --- a/app/assets/css/style.css +++ b/app/assets/css/style.css @@ -2,6 +2,6 @@ @import 'fonts.css'; @import 'global.css'; @import '../../components/Login.css'; -@import '../../components/LoggedIn.css'; +@import '../../components/Connect.css'; @import '../../components/HeaderBar.css'; @import '../../components/Layout.css'; diff --git a/app/assets/images/app-triangle-error.svg b/app/assets/images/app-triangle-error.svg new file mode 100644 index 0000000000..2914d7e389 --- /dev/null +++ b/app/assets/images/app-triangle-error.svg @@ -0,0 +1,5 @@ +<svg width="30px" height="13px" viewBox="0 0 30 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <title>app-triangle-extended</title> + <desc>Mullvad VPN app</desc> + <path fill="#D0021B" d="M0,12 L30,12 L30,13 L0,13 L0,12 Z M0,12 C7.24137931,12 12.9310345,1.0135008e-16 15,0 C17.0689655,0 23.7931034,12 30,12 L0,12 Z" id="app-triangle-extended"></path> +</svg>
\ No newline at end of file diff --git a/app/assets/images/app-triangle-success.svg b/app/assets/images/app-triangle-success.svg new file mode 100644 index 0000000000..711e99afd7 --- /dev/null +++ b/app/assets/images/app-triangle-success.svg @@ -0,0 +1,5 @@ +<svg width="30px" height="13px" viewBox="0 0 30 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <title>app-triangle-extended</title> + <desc>Mullvad VPN app</desc> + <path fill="#44AD4D" d="M0,12 L30,12 L30,13 L0,13 L0,12 Z M0,12 C7.24137931,12 12.9310345,1.0135008e-16 15,0 C17.0689655,0 23.7931034,12 30,12 L0,12 Z" id="app-triangle-extended"></path> +</svg>
\ No newline at end of file diff --git a/app/assets/images/app-triangle.svg b/app/assets/images/app-triangle.svg new file mode 100644 index 0000000000..e2f7ca044b --- /dev/null +++ b/app/assets/images/app-triangle.svg @@ -0,0 +1,5 @@ +<svg width="30px" height="13px" viewBox="0 0 30 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <title>app-triangle-extended</title> + <desc>Mullvad VPN app</desc> + <path fill="#294D73" d="M0,12 L30,12 L30,13 L0,13 L0,12 Z M0,12 C7.24137931,12 12.9310345,1.0135008e-16 15,0 C17.0689655,0 23.7931034,12 30,12 L0,12 Z" id="app-triangle-extended"></path> +</svg>
\ No newline at end of file diff --git a/app/assets/images/trayicon.png b/app/assets/images/trayIconTemplate.png Binary files differindex 3888745a8f..3888745a8f 100644 --- a/app/assets/images/trayicon.png +++ b/app/assets/images/trayIconTemplate.png diff --git a/app/assets/images/trayicon@2x.png b/app/assets/images/trayIconTemplate@2x.png Binary files differindex 3b5f364903..3b5f364903 100644 --- a/app/assets/images/trayicon@2x.png +++ b/app/assets/images/trayIconTemplate@2x.png diff --git a/app/components/Connect.js b/app/components/Connect.js index dc32594067..27aaedd4ee 100644 --- a/app/components/Connect.js +++ b/app/components/Connect.js @@ -1,14 +1,19 @@ -import React, { Component } from 'react'; +import React, { Component, PropTypes } from 'react'; import { Layout, Container, Header } from './Layout'; export default class Connect extends Component { + + static propTypes = { + logout: PropTypes.func.isRequired + } + render() { return ( <Layout> <Header /> <Container> <div className="connect"> - + <button style={{ width: '100px', display: 'block', margin: '10px auto' }} onClick={ this.props.logout }>Log out</button> </div> </Container> </Layout> diff --git a/app/components/HeaderBar.css b/app/components/HeaderBar.css index 4d9d326eff..140e64df55 100644 --- a/app/components/HeaderBar.css +++ b/app/components/HeaderBar.css @@ -1,15 +1,40 @@ .headerbar { + margin-top: 12px; padding: 12px; + background-color: #294D73; + border-radius: 8px 8px 0 0; + position: relative; } -.header--style-error { +.headerbar:before { + display: block; + content: ''; + width: 30px; + height: 13px; + margin: -12px 0 0 -15px; + position: absolute; + left: 50%; + top: 0%; + background-image: url(../assets/images/app-triangle.svg); + background-repeat: no-repeat; +} + +.headerbar--style-error { background-color: #D0021B; } -.header--style-success { +.headerbar--style-error:before { + background-image: url(../assets/images/app-triangle-error.svg); +} + +.headerbar--style-success { background-color: #44AD4D; } +.headerbar--style-success:before { + background-image: url(../assets/images/app-triangle-success.svg); +} + .headerbar__title { font-family: DINPro; font-size: 24px; diff --git a/app/components/HeaderBar.js b/app/components/HeaderBar.js index d13c5c3462..ce5d039f1f 100644 --- a/app/components/HeaderBar.js +++ b/app/components/HeaderBar.js @@ -15,7 +15,7 @@ export default class HeaderBar extends Component { let containerClass = ['headerbar']; if(HeaderBar.Style.isValid(style)) { - containerClass.push(`header--style-${style}`); + containerClass.push(`headerbar--style-${style}`); } return ( diff --git a/app/components/Layout.css b/app/components/Layout.css index 8ef242d7f5..5d1aa92445 100644 --- a/app/components/Layout.css +++ b/app/components/Layout.css @@ -2,7 +2,6 @@ display: flex; flex-direction: column; height: 100vh; - background: #294D73; } .layout__header { @@ -11,4 +10,5 @@ .layout__container { flex: 1 1 100%; + background: #294D73; }
\ No newline at end of file diff --git a/app/components/Layout.js b/app/components/Layout.js index b0cc0a8420..50f52e7dca 100644 --- a/app/components/Layout.js +++ b/app/components/Layout.js @@ -10,7 +10,7 @@ export class Header extends Component { <div className="layout__header"> <HeaderBar {...this.props} /> </div> - ) + ); } } @@ -24,7 +24,7 @@ export class Container extends Component { <div className="layout__container"> {this.props.children} </div> - ) + ); } } diff --git a/app/components/Tray.js b/app/components/Tray.js deleted file mode 100644 index 00dc679981..0000000000 --- a/app/components/Tray.js +++ /dev/null @@ -1,39 +0,0 @@ -import React, { Component, PropTypes } from 'react'; -import { TrayMenu, TrayItem } from '../lib/components/TrayMenu'; -import { shell } from 'electron'; - -import { LoginState } from '../constants'; - -export default class Tray extends Component { - - static propTypes = { - handle: PropTypes.object.isRequired, - backend: PropTypes.object.isRequired - } - - logout() { - this.props.logout(this.props.backend); - } - - openPrivacyPolicy() { - shell.openExternal('https://mullvad.net/#privacy'); - } - - openHomepage() { - shell.openExternal('https://mullvad.net'); - } - - render() { - const loggedIn = this.props.user.status === LoginState.ok; - - return ( - <TrayMenu tray={ this.props.handle }> - <TrayItem label="Log out" click={ ::this.logout } visible={ loggedIn } /> - <TrayItem type="separator" visible={ loggedIn } /> - <TrayItem label="Privacy Policy" click={ ::this.openPrivacyPolicy } /> - <TrayItem label="Visit homepage" click={ ::this.openHomepage } /> - </TrayMenu> - ); - } - -}
\ No newline at end of file diff --git a/app/containers/ConnectPage.js b/app/containers/ConnectPage.js index 9aad1f81f5..9802e8b2d8 100644 --- a/app/containers/ConnectPage.js +++ b/app/containers/ConnectPage.js @@ -1,12 +1,19 @@ import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; import Connect from '../components/Connect'; +import userActions from '../actions/user'; const mapStateToProps = (state) => { return state; }; -const mapDispatchToProps = (dispatch) => { // eslint-disable-line no-unused-vars - return {}; +const mapDispatchToProps = (dispatch, props) => { // eslint-disable-line no-unused-vars + const user = bindActionCreators(userActions, dispatch); + return { + logout: () => { + return user.logout(props.backend); + } + }; }; export default connect(mapStateToProps, mapDispatchToProps)(Connect); diff --git a/app/containers/Tray.js b/app/containers/Tray.js deleted file mode 100644 index 3916263d0d..0000000000 --- a/app/containers/Tray.js +++ /dev/null @@ -1,14 +0,0 @@ -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import Tray from '../components/Tray'; -import userActions from '../actions/user'; - -const mapStateToProps = (state) => { - return state; -}; - -const mapDispatchToProps = (dispatch) => { - return bindActionCreators(userActions, dispatch); -}; - -export default connect(mapStateToProps, mapDispatchToProps)(Tray); diff --git a/app/lib/components/TrayMenu.js b/app/lib/components/TrayMenu.js deleted file mode 100644 index 5f14ca71f9..0000000000 --- a/app/lib/components/TrayMenu.js +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Declarative Tray implementation for React + Electron - */ - -import React, { Component, PropTypes } from 'react'; -import { remote } from 'electron'; - -const { Menu, MenuItem } = remote; - -/** - * Tray menu component - * - * Example: - * - * const tray = new remote.Tray('/path/to/icon'); - * - * return ( - * <TrayMenu tray={tray}> - * <TrayItem label="Visit homepage" /> - * </TrayMenu> - * ) - */ -export class TrayMenu extends Component { - - static childContextTypes = { - menu: PropTypes.object.isRequired - }; - - static propTypes = { - tray: PropTypes.object.isRequired, - children: PropTypes.arrayOf(PropTypes.node).isRequired - }; - - _contextMenu = null; - - getChildContext() { - return { menu: this._contextMenu }; - } - - componentDidMount() { - this.props.tray.setContextMenu(this._contextMenu); - } - - componentDidUpdate() { - this.props.tray.setContextMenu(this._contextMenu); - } - - render() { - // create new menu during each rendering - // see: https://github.com/electron/electron/issues/8598 - this._contextMenu = new Menu(); - - return ( - <div>{this.props.children}</div> - ); - } - -} - -/** - * Submenu component - * - * Example: - * - * <TrayMenu tray={this.props.handle}> - * <TraySubmenu label="Resources"> - * <TrayItem label="Homepage" /> - * </TraySubmenu> - * </TrayMenu> - * - */ -export class TraySubmenu extends Component { - - static contextTypes = { - menu: PropTypes.object.isRequired - }; - - static childContextTypes = { - menu: PropTypes.object.isRequired - }; - - static propTypes = { - children: PropTypes.arrayOf(PropTypes.node).isRequired - }; - - _contextMenu = null; - - getChildContext() { - return { menu: this._contextMenu }; - } - - render() { - // create new menu during each rendering - // see: https://github.com/electron/electron/issues/8598 - this._contextMenu = new Menu(); - - this.context.menu.append(new MenuItem({ ...this.props, submenu: this._contextMenu })); - - return ( - <div>{this.props.children}</div> - ); - } - -} - -/** - * Item component - */ -export class TrayItem extends Component { - - static contextTypes = { - menu: PropTypes.object.isRequired - }; - - render() { - this.context.menu.append(new MenuItem(this.props)); - return null; - } - -} diff --git a/app/main.js b/app/main.js index caea31ab57..81e5d39e4c 100644 --- a/app/main.js +++ b/app/main.js @@ -1,11 +1,13 @@ import path from 'path'; -import url from 'url'; -import { app, crashReporter, BrowserWindow, Menu } from 'electron'; +import { app, crashReporter, BrowserWindow, ipcMain, Tray, Menu } from 'electron'; const isDevelopment = (process.env.NODE_ENV === 'development'); -let mainWindow = null; -let forceQuit = false; +let window = null; +let tray = null; + +// hide dock icon +app.dock.hide(); const installExtensions = async () => { const installer = require('electron-devtools-installer'); @@ -14,7 +16,7 @@ const installExtensions = async () => { 'REDUX_DEVTOOLS' ]; const forceDownload = !!process.env.UPGRADE_EXTENSIONS; - for (const name of extensions) { + for(const name of extensions) { try { await installer.default(installer[name], forceDownload); } catch (e) { @@ -23,86 +25,122 @@ const installExtensions = async () => { } }; -crashReporter.start({ - productName: 'YourName', - companyName: 'YourCompany', - submitURL: 'https://your-domain.com/url-to-submit', - uploadToServer: false -}); +const installDevTools = async () => { + await installExtensions(); -app.on('window-all-closed', () => { - // On OS X it is common for applications and their menu bar - // to stay active until the user quits explicitly with Cmd + Q - if (process.platform !== 'darwin') { - app.quit(); - } -}); + // show devtools when ctrl clicked + tray.on('click', function () { + if(!window) { return; } -app.on('ready', async () => { - if (isDevelopment) { - await installExtensions(); - } + if(window.isDevToolsOpened()) { + window.devToolsWebContents.focus(); + } else { + window.openDevTools({ mode: 'detach' }); + } + }); + + // add inspect element on right click menu + window.webContents.on('context-menu', (e, props) => { + Menu.buildFromTemplate([{ + label: 'Inspect element', + click() { + window.openDevTools({ mode: 'detach' }); + window.inspectElement(props.x, props.y); + } + }]).popup(window); + }); +}; + +const getWindowPosition = () => { + const windowBounds = window.getBounds(); + const trayBounds = tray.getBounds(); + + // center window horizontally below the tray icon + const x = Math.round(trayBounds.x + (trayBounds.width / 2) - (windowBounds.width / 2)); - mainWindow = new BrowserWindow({ + // position window vertically below the tray icon + const y = Math.round(trayBounds.y + trayBounds.height); + + return { x, y }; +}; + +const createWindow = () => { + window = new BrowserWindow({ width: 320, - height: 568 + 20, // 20pt window chrome - show: false, - backgroundColor: '#000000', + height: 568, + frame: false, resizable: false, maximizable: false, - fullscreenable: false + fullscreenable: false, + transparent: true, + show: false, + webPreferences: { + // prevents renderer process code from not running when window is hidden + backgroundThrottling: false + } }); - mainWindow.loadURL(url.format({ - pathname: path.join(__dirname, 'index.html'), - protocol: 'file:', - slashes: true - })); + window.loadURL('file://' + path.join(__dirname, 'index.html')); - // show window once on first load - mainWindow.webContents.once('did-finish-load', () => { - mainWindow.show(); + // hide the window when it loses focus + window.on('blur', () => { + if(!window.webContents.isDevToolsOpened()) { + window.hide(); + } }); - mainWindow.webContents.on('did-finish-load', () => { - // Handle window logic properly on macOS: - // 1. App should not terminate if window has been closed - // 2. Click on icon in dock should re-open the window - // 3. ⌘+Q should close the window and quit the app - if (process.platform === 'darwin') { - mainWindow.on('close', function (e) { - if (!forceQuit) { - e.preventDefault(); - mainWindow.hide(); - } - }); + window.on('show', () => { + tray.setHighlightMode('always'); + }); - app.on('activate', () => { - mainWindow.show(); - }); - - app.on('before-quit', () => { - forceQuit = true; - }); - } else { - mainWindow.on('closed', () => { - mainWindow = null; - }); - } + window.on('hide', () => { + tray.setHighlightMode('never'); }); - if (isDevelopment) { - // auto-open dev tools - mainWindow.webContents.openDevTools(); +}; + +const toggleWindow = () => { + if (window.isVisible()) { + window.hide(); + } else { + showWindow(); + } +}; + +const showWindow = () => { + const position = getWindowPosition(); + window.setPosition(position.x, position.y, false); + window.show(); + window.focus(); +}; + +ipcMain.on('show-window', () => { + showWindow(); +}); + +const createTray = () => { + tray = new Tray(path.join(__dirname, 'assets/images/trayIconTemplate.png')); + tray.on('right-click', toggleWindow); + tray.on('double-click', toggleWindow); + tray.on('click', toggleWindow); +}; + +crashReporter.start({ + productName: 'YourName', + companyName: 'YourCompany', + submitURL: 'https://your-domain.com/url-to-submit', + uploadToServer: false +}); + +app.on('window-all-closed', () => { + app.quit(); +}); + +app.on('ready', () => { + createTray(); + createWindow(); - // add inspect element on right click menu - mainWindow.webContents.on('context-menu', (e, props) => { - Menu.buildFromTemplate([{ - label: 'Inspect element', - click() { - mainWindow.inspectElement(props.x, props.y); - } - }]).popup(mainWindow); - }); + if(isDevelopment) { + installDevTools(); } }); diff --git a/app/store.js b/app/store.js index 350650631b..55a3df85f8 100644 --- a/app/store.js +++ b/app/store.js @@ -1,6 +1,6 @@ import { createStore, applyMiddleware, combineReducers, compose } from 'redux'; import { hashHistory } from 'react-router'; -import { routerMiddleware, routerReducer as routing, push } from 'react-router-redux'; +import { routerMiddleware, routerReducer as routing, push, replace } from 'react-router-redux'; import persistState from 'redux-localstorage'; import thunk from 'redux-thunk'; @@ -11,7 +11,8 @@ const router = routerMiddleware(hashHistory); const actionCreators = { ...userActions, - push + pushRoute: (route) => push(route), + replaceRoute: (route) => replace(route), }; const reducers = { |
