diff options
| author | Andrej Mihajlov <and@codeispoetry.ru> | 2017-03-16 21:16:31 +0000 |
|---|---|---|
| committer | Andrej Mihajlov <and@codeispoetry.ru> | 2017-03-16 21:16:31 +0000 |
| commit | 627c285b8409a8872208f54e8b9f3c3e3f4bd34d (patch) | |
| tree | fcd9b116d038ca1b12de34d7a80184208c1e3046 | |
| parent | 0fca9f68baba1ef13cfa76b8316bf93f37d6f741 (diff) | |
| download | mullvadvpn-627c285b8409a8872208f54e8b9f3c3e3f4bd34d.tar.xz mullvadvpn-627c285b8409a8872208f54e8b9f3c3e3f4bd34d.zip | |
Refactor code
| -rw-r--r-- | app/app.js | 12 | ||||
| -rw-r--r-- | app/enums.js | 9 | ||||
| -rw-r--r-- | app/lib/tray-animation.js | 271 | ||||
| -rw-r--r-- | app/lib/tray-animator.js | 176 | ||||
| -rw-r--r-- | app/lib/tray-icon-manager.js | 99 | ||||
| -rw-r--r-- | app/lib/tray-icon-provider.js | 100 | ||||
| -rw-r--r-- | app/main.js | 63 | ||||
| -rw-r--r-- | test/tray-animator.spec.js | 3 |
8 files changed, 499 insertions, 234 deletions
diff --git a/app/app.js b/app/app.js index 114b5e276e..468deca4df 100644 --- a/app/app.js +++ b/app/app.js @@ -12,7 +12,7 @@ import connectActions from './actions/connect'; import Backend from './lib/backend'; import mapBackendEventsToReduxActions from './lib/backend-redux-actions'; import mapBackendEventsToRouter from './lib/backend-routing'; -import { LoginState, ConnectionState } from './enums'; +import { LoginState, ConnectionState, TrayIconType } from './enums'; const initialState = {}; const memoryHistory = createMemoryHistory(); @@ -45,15 +45,15 @@ if(recentLocation && recentLocation.pathname) { // Tray icon const updateTrayIcon = () => { - const getName = (s) => { + const getIconType = (s) => { switch(s) { - case ConnectionState.connected: return 'secured'; - case ConnectionState.connecting: return 'securing'; - default: return 'unsecured'; + case ConnectionState.connected: return TrayIconType.secured; + case ConnectionState.connecting: return TrayIconType.securing; + default: return TrayIconType.unsecured; } }; const { connect } = store.getState(); - ipcRenderer.send('changeTrayIcon', getName(connect.status)); + ipcRenderer.send('changeTrayIcon', getIconType(connect.status)); }; // patch backend diff --git a/app/enums.js b/app/enums.js index bc14ba7258..399c4602b4 100644 --- a/app/enums.js +++ b/app/enums.js @@ -19,3 +19,12 @@ export const LoginState = new Enum('none', 'connecting', 'failed', 'ok'); * @property {string} failed Failed to connect */ export const ConnectionState = new Enum('disconnected', 'connecting', 'connected', 'failed'); + +/** + * Tray icon type + * @type {TrayIconType} + * @property {string} unsecured - Initial state (unlocked) + * @property {string} securing - Securing network (spinner) + * @property {string} secured - Connection is secured (locked) + */ +export const TrayIconType = new Enum('unsecured', 'securing', 'secured'); diff --git a/app/lib/tray-animation.js b/app/lib/tray-animation.js new file mode 100644 index 0000000000..82496033ab --- /dev/null +++ b/app/lib/tray-animation.js @@ -0,0 +1,271 @@ +import assert from 'assert'; +import fs from 'fs'; +import path from 'path'; +import { nativeImage } from 'electron'; + +/** + * Tray animation descriptor + * + * @export + * @class TrayAnimation + * @property {number} speed - speed per frame + * @property {bool} repeat - whether to repeat animation + * @property {bool} reverse - play in reverse + * @property {bool} alternate - whether to alternate sequence when reached the end of animation + * @property {string[]} source - image source + * @property {electron.NativeImage} nativeImages - a sequence of native images + * @property {bool} isFinished - whether animation sequence is finished (repeating animation never finish) + */ +export class TrayAnimation { + + /** + * Set animation pace per frame in ms + * + * @memberOf TrayAnimation + */ + set speed(v) { this._speed = parseInt(v); } + + /** + * Get animation pace per frame in ms + * + * @readonly + * + * @memberOf TrayAnimation + */ + get speed() { return this._speed; } + + /** + * Set animation repetition + * @memberOf TrayAnimation + */ + set repeat(v) { this._repeat = !!v; } + + /** + * Get animation repetition + * + * @readonly + * + * @memberOf TrayAnimation + */ + get repeat() { return this._repeat; } + + /** + * Set animation reversal + * @memberOf TrayAnimation + */ + set reverse(v) { this._reverse = !!v; } + + /** + * Get animation reversal + * + * @readonly + * + * @memberOf TrayAnimation + */ + get reverse() { return this._repeat; } + + /** + * Set animation alternation + * + * @memberOf TrayAnimation + */ + set alternate(v) { this._alternate = !!v; } + + /** + * Get animation alternation + * + * @readonly + * + * @memberOf TrayAnimation + */ + get alternate() { return this._alternate; } + + /** + * Source array of images + * + * @readonly + * + * @memberOf TrayAnimation + */ + get source() { return this._source.slice(); } + + /** + * Array of NativeImage instances loaded based on source input + * + * @readonly + * + * @memberOf TrayAnimation + */ + get nativeImages() { return this._nativeImages.slice(); } + + /** + * Flag that tells whether animation finished + * + * @readonly + * + * @memberOf TrayAnimation + */ + get isFinished() { return this._isFinished; } + + /** + * Create animation using file sequence + * + * @static + * @param {string} filePattern - file name pattern where {s} is replaced with index + * @param {number[]} range - sequence range [start, end] + * + * @memberOf TrayAnimation + * @return {TrayAnimation} + */ + static fromFileSequence(filePattern, range) { + assert(range.length === 2 && range[0] < range[1]); + + let images = []; + for(let i = range[0]; i <= range[1]; i++) { + images.push(filePattern.replace('{s}', i)); + } + + return new TrayAnimation(images); + } + + /** + * Creates an instance of TrayAnimation. + * @param {string[]} images + * + * @memberOf TrayAnimation + */ + constructor(images) { + assert(images.length > 0); + + this._source = images.slice(); + this._nativeImages = images.map((pathOrNativeImage) => { + if(typeof(pathOrNativeImage) === 'string') { + return nativeImage.createFromPath(pathOrNativeImage); + } else if(typeof(pathOrNativeImage) === 'NativeImage') { + return pathOrNativeImage; + } + return nativeImage.createEmpty(); + }); + + this._speed = 200; // ms + this._repeat = false; + this._reverse = false; + this._alternate = false; + + this._numFrames = images.length; + this._currentFrame = 0; + this._isFinished = false; + } + + /** + * Get current sprite + * + * @readonly + * + * @memberOf TrayAnimation + */ + get currentImage() { + return this._nativeImages[this._currentFrame]; + } + + /** + * Prepare initial state for animation before running it. + * @memberOf TrayAnimation + */ + prepare() { + this._currentFrame = this._firstFrame(this._reverse); + } + + /** + * Advance animation to the start + * This method respects animation reversal + * + * @memberOf TrayAnimation + */ + advanceToStart() { + this._currentFrame = this._firstFrame(this._reverse); + } + + /** + * Advance animation to the end + * This method respects animation reversal + * + * @memberOf TrayAnimation + */ + advanceToEnd() { + this._currentFrame = this._lastFrame(this._reverse); + } + + /** + * Advance animation frame + * @memberOf TrayAnimation + */ + advanceFrame() { + // do not advance frame when animation is finished + if(this._isFinished) { return; } + + // advance frame + let nextFrame = this._nextFrame(this._currentFrame, this._reverse); + + // did reach end? + if(nextFrame < 0 || nextFrame >= this._numFrames) { + // mark animation as finished if it's not marked as repeating + if(!this._repeat) { + this._isFinished = true; + return; + } + + // change animation direction if marked for alternation + if(this._alternate) { + this._reverse = !this._reverse; + + // clamp range + nextFrame = Math.min(Math.max(0, nextFrame), this._numFrames - 1); + + // skip corner frame when alternating by advancing frame once again + nextFrame = this._nextFrame(nextFrame, this._reverse); + } else { + nextFrame = this._firstFrame(this._reverse); + } + } + this._currentFrame = nextFrame; + } + + /** + * Calculate next frame + * @private + * @param {number} cur - current frame + * @param {bool} isReverse - reverse sequence direction? + * @returns {number} + * + * @memberOf TrayAnimation + */ + _nextFrame(cur, isReverse) { + return cur + (isReverse ? -1 : 1); + } + + /** + * Get first frame of animation + * + * @param {bool} isReverse reverse animation? + * @returns {number} + * + * @memberOf TrayAnimation + */ + _firstFrame(isReverse) { + return isReverse ? this._numFrames - 1 : 0; + } + + /** + * Get last frame of animation + * + * @param {bool} isReverse reverse animation? + * @returns {number} + * + * @memberOf TrayAnimation + */ + _lastFrame(isReverse) { + return isReverse ? 0 : this._numFrames - 1; + } + +}
\ No newline at end of file diff --git a/app/lib/tray-animator.js b/app/lib/tray-animator.js index c4c00628c5..601362d96d 100644 --- a/app/lib/tray-animator.js +++ b/app/lib/tray-animator.js @@ -4,182 +4,6 @@ import path from 'path'; import { nativeImage } from 'electron'; /** - * Tray animation descriptor - * - * @export - * @class TrayAnimation - * @property {number} speed - speed per frame - * @property {bool} repeat - whether to repeat animation - * @property {bool} reverse - play in reverse - * @property {bool} alternate - whether to alternate sequence when reached the end of animation - * @property {string[]} source - image source - * @property {electron.NativeImage} nativeImages - a sequence of native images - * @property {bool} isFinished - whether animation sequence is finished (repeating animation never finish) - */ -export class TrayAnimation { - - set speed(v) { this._speed = parseInt(v); } - get speed() { return this._speed; } - - set repeat(v) { this._repeat = !!v; } - get repeat() { return this._repeat; } - - set reverse(v) { this._reverse = !!v; } - get reverse() { return this._repeat; } - - set alternate(v) { this._alternate = !!v; } - get alternate() { return this._alternate; } - - get source() { return this._source.slice(); } - get nativeImages() { return this._nativeImages.slice(); } - - get isFinished() { return this._isFinished; } - - /** - * Create animation using file sequence - * - * @static - * @param {string} filePattern - file name pattern where {s} is replaced with index - * @param {number[]} range - sequence range [start, end] - * - * @memberOf TrayAnimation - * @return {TrayAnimation} - */ - static fromFileSequence(filePattern, range) { - assert(range.length === 2 && range[0] < range[1]); - - let images = []; - for(let i = range[0]; i <= range[1]; i++) { - images.push(filePattern.replace('{s}', i)); - } - - return new TrayAnimation(images); - } - - /** - * Creates an instance of TrayAnimation. - * @param {string[]} images - * - * @memberOf TrayAnimation - */ - constructor(images) { - assert(images.length > 0); - - this._source = images.slice(); - this._nativeImages = images.map((pathOrNativeImage) => { - if(typeof(pathOrNativeImage) === 'string') { - return nativeImage.createFromPath(pathOrNativeImage); - } else if(typeof(pathOrNativeImage) === 'NativeImage') { - return pathOrNativeImage; - } - return nativeImage.createEmpty(); - }); - - this._speed = 200; // ms - this._repeat = false; - this._reverse = false; - this._alternate = false; - - this._numFrames = images.length; - this._currentFrame = 0; - this._isFinished = false; - } - - get currentImage() { - return this._nativeImages[this._currentFrame]; - } - - /** - * Prepare initial state for animation before running it. - * @memberOf TrayAnimation - */ - prepare() { - this._currentFrame = this._firstFrame(this._reverse); - } - - advanceToStart() { - this._currentFrame = this._firstFrame(this._reverse); - } - - advanceToEnd() { - this._currentFrame = this._lastFrame(this._reverse); - } - - /** - * Advance animation frame - * @memberOf TrayAnimation - */ - advanceFrame() { - // do not advance frame when animation is finished - if(this._isFinished) { return; } - - // advance frame - let nextFrame = this._nextFrame(this._currentFrame, this._reverse); - - // did reach end? - if(nextFrame < 0 || nextFrame >= this._numFrames) { - // mark animation as finished if it's not marked as repeating - if(!this._repeat) { - this._isFinished = true; - return; - } - - // change animation direction if marked for alternation - if(this._alternate) { - this._reverse = !this._reverse; - - // clamp range - nextFrame = Math.min(Math.max(0, nextFrame), this._numFrames - 1); - - // skip corner frame when alternating by advancing frame once again - nextFrame = this._nextFrame(nextFrame, this._reverse); - } else { - nextFrame = this._firstFrame(this._reverse); - } - } - this._currentFrame = nextFrame; - } - - /** - * Calculate next frame - * @private - * @param {number} cur - current frame - * @param {bool} isReverse - reverse sequence direction? - * @returns {number} - * - * @memberOf TrayAnimation - */ - _nextFrame(cur, isReverse) { - return cur + (isReverse ? -1 : 1); - } - - /** - * Get first frame of animation - * - * @param {bool} isReverse reverse animation? - * @returns {number} - * - * @memberOf TrayAnimation - */ - _firstFrame(isReverse) { - return isReverse ? this._numFrames - 1 : 0; - } - - /** - * Get last frame of animation - * - * @param {bool} isReverse reverse animation? - * @returns {number} - * - * @memberOf TrayAnimation - */ - _lastFrame(isReverse) { - return isReverse ? 0 : this._numFrames - 1; - } - -} - -/** * Tray icon animator * @class TrayAnimator */ diff --git a/app/lib/tray-icon-manager.js b/app/lib/tray-icon-manager.js new file mode 100644 index 0000000000..e72c348add --- /dev/null +++ b/app/lib/tray-icon-manager.js @@ -0,0 +1,99 @@ +import assert from 'assert'; +import { TrayAnimator } from './tray-animator'; +import { TrayIconType } from '../enums'; + +/** + * Tray icon manager + * + * @export + * @class TrayIconManager + */ +export default class TrayIconManager { + + /** + * Creates an instance of TrayIconManager. + * @param {electron.Tray} tray + * @param {TrayIconProvider} iconProvider + * + * @memberOf TrayIconManager + */ + constructor(tray, iconProvider) { + assert(tray); + assert(iconProvider); + + this._tray = tray; + this._iconProvider = iconProvider; + this._animator = null; + this._iconType = null; + } + + /** + * Destroy manager + * @memberOf TrayIconManager + */ + destroy() { + if(this._animator) { + this._animator.stop(); + this._animator = null; + } + this._iconType = null; + } + + /** + * Get current icon type + * @memberOf TrayIconManager + */ + get iconType() { + return this._iconType; + } + + /** + * Set current icon type + * @memberOf TrayIconManager + */ + set iconType(type) { + let animator; + assert(TrayIconType.isValid(type)); + + // no-op if same animator requested + if(this._iconType === type) { return; } + + // destroy existing animator + if(this._animator) { + this._animator.stop(); + this._animator = null; + } + + // do not animate if setting icon for the first time + const skipAnimation = this._iconType === null; + + switch(type) { + case TrayIconType.secured: + animator = new TrayAnimator(this._tray, this._iconProvider.lockAnimation()); + if(skipAnimation) { + animator.advanceToEnd(); + } else { + animator.start(); + } + break; + + case TrayIconType.unsecured: + animator = new TrayAnimator(this._tray, this._iconProvider.unlockAnimation()); + if(skipAnimation) { + animator.advanceToStart(); + } else { + animator.start(); + } + break; + + case TrayIconType.securing: + animator = new TrayAnimator(this._tray, this._iconProvider.spinnerAnimation()); + animator.start(); + break; + } + + this._animator = animator; + this._iconType = type; + } + +}
\ No newline at end of file diff --git a/app/lib/tray-icon-provider.js b/app/lib/tray-icon-provider.js new file mode 100644 index 0000000000..cf859852eb --- /dev/null +++ b/app/lib/tray-icon-provider.js @@ -0,0 +1,100 @@ +import path from 'path'; +import { systemPreferences } from 'electron'; +import { TrayAnimation } from './tray-animation'; + +const menubarIcons = { + base: path.join(path.resolve(__dirname, '..'), 'assets/images/menubar icons'), + spinner: { + light: 'light ui/spinner/spinner-{s}-light.png', + dark: 'dark ui/spinner/spinner-{s}-dark.png' + }, + lock: { + light: 'light ui/lock/lock-{s}-light.png', + dark: 'dark ui/lock/lock-{s}-dark.png' + } +}; + +/** + * Tray icon provider + * + * @export + * @class TrayIconProvider + */ +export default class TrayIconProvider { + + /** + * Get lock animation + * + * @param {boolean} [isReverse=false] whether animation should be reversed + * @returns TrayIconAnimator + * + * @memberOf TrayIconProvider + */ + lockAnimation(isReverse = false) { + let animation = TrayAnimation.fromFileSequence(this._lockPath, [1, 9]); + animation.speed = 100; + animation.reverse = isReverse; + + return animation; + } + + /** + * Get unlock animation + * + * @returns TrayIconAnimator + * + * @memberOf TrayIconProvider + */ + unlockAnimation() { + return this.lockAnimation(true); + } + + /** + * Get spinner animation + * + * @returns TrayIconAnimator + * + * @memberOf TrayIconProvider + */ + spinnerAnimation() { + let animation = TrayAnimation.fromFileSequence(this._spinnerPath, [1, 9]); + animation.speed = 100; + animation.repeat = true; + + return animation; + } + + /** + * Pattern to spinner animation assets based on macOS menubar theme + * + * @readonly + * + * @memberOf TrayIconProvider + */ + get _spinnerPath() { + return path.join(menubarIcons.base, menubarIcons.spinner[this._colorTheme]); + } + + /** + * Pattern to lock/unlock animation assets based on macOS menubar theme + * + * @readonly + * + * @memberOf TrayIconProvider + */ + get _lockPath() { + return path.join(menubarIcons.base, menubarIcons.lock[this._colorTheme]); + } + + /** + * Current theme name based on macOS menubar theme. + * + * @readonly + * + * @memberOf TrayIconProvider + */ + get _colorTheme() { + return systemPreferences.isDarkMode() ? 'dark' : 'light'; + } + +}
\ No newline at end of file diff --git a/app/main.js b/app/main.js index 582b14c68d..56cf8b6967 100644 --- a/app/main.js +++ b/app/main.js @@ -1,6 +1,7 @@ import path from 'path'; -import { app, crashReporter, BrowserWindow, ipcMain, Tray, Menu } from 'electron'; -import { TrayAnimation, TrayAnimator } from './lib/tray-animator'; +import { app, crashReporter, BrowserWindow, ipcMain, Tray, Menu, nativeImage } from 'electron'; +import TrayIconManager from './lib/tray-icon-manager'; +import TrayIconProvider from './lib/tray-icon-provider'; // Override appData path to avoid collisions with old client // New userData path, i.e on macOS: ~/Library/Application Support/mullvad.vpn @@ -13,6 +14,7 @@ const isDevelopment = (process.env.NODE_ENV === 'development'); let window = null; let tray = null; let macEventMonitor = null; +let trayIconManager = null; const startTrayEventMonitor = (win) => { if(process.platform === 'darwin') { @@ -30,52 +32,8 @@ const stopTrayEventMonitor = () => { } }; -const menubarIcons = { - base: path.join(__dirname, 'assets/images/menubar icons'), - spinner: { - light: 'light ui/spinner/spinner-{s}-light.png', - dark: 'dark ui/spinner/spinner-{s}-dark.png' - }, - lock: { - light: 'light ui/lock/lock-{s}-light.png', - dark: 'dark ui/lock/lock-{s}-dark.png' - } -}; - -const spinnerPath = path.join(menubarIcons.base, menubarIcons.spinner.light); -const lockPath = path.join(menubarIcons.base, menubarIcons.lock.light); - -let trayAnimator = null; - -ipcMain.on('changeTrayIcon', (event, name) => { - if(!tray) { return; } - - trayAnimator && trayAnimator.stop(); - trayAnimator = null; - - console.log('changeTrayIcon: ' + name); - - if(name === 'securing') { - let animation = TrayAnimation.fromFileSequence(spinnerPath, [1, 9]); - animation.speed = 100; - animation.repeat = true; - - trayAnimator = new TrayAnimator(tray, animation); - trayAnimator.start(); - } else if(name === 'secured') { - let animation = TrayAnimation.fromFileSequence(lockPath, [1, 9]); - animation.speed = 100; - - trayAnimator = new TrayAnimator(tray, animation); - trayAnimator.start(); - } else if(name === 'unsecured') { - let animation = TrayAnimation.fromFileSequence(lockPath, [1, 9]); - animation.speed = 100; - animation.reverse = true; - - trayAnimator = new TrayAnimator(tray, animation); - trayAnimator.start(); - } +ipcMain.on('changeTrayIcon', (event, type) => { + trayIconManager.iconType = type; }); // hide dock icon @@ -171,12 +129,10 @@ const createWindow = () => { }); window.on('show', () => { - tray.setHighlightMode('always'); startTrayEventMonitor(window); }); window.on('hide', () => { - tray.setHighlightMode('never'); stopTrayEventMonitor(); }); @@ -198,10 +154,15 @@ const showWindow = () => { }; const createTray = () => { - tray = new Tray(path.join(__dirname, 'assets/images/tray-icon-default.png')); + tray = new Tray(nativeImage.createEmpty()); tray.on('right-click', toggleWindow); tray.on('double-click', toggleWindow); tray.on('click', toggleWindow); + + // never highlight menu + tray.setHighlightMode('never'); + + trayIconManager = new TrayIconManager(tray, new TrayIconProvider()); }; crashReporter.start({ diff --git a/test/tray-animator.spec.js b/test/tray-animator.spec.js index 275215413d..c6c1769160 100644 --- a/test/tray-animator.spec.js +++ b/test/tray-animator.spec.js @@ -1,5 +1,6 @@ import { expect } from 'chai'; -import { TrayAnimator, TrayAnimation } from '../app/lib/tray-animator'; +import { TrayAnimator, } from '../app/lib/tray-animator'; +import { TrayAnimation } from '../app/lib/tray-animation'; import { nativeImage } from 'electron'; describe('lib/tray-animator', function() { |
