diff --git a/lib/browser/app.js b/lib/browser/app.js index 6e9cef30..531331f2 100644 --- a/lib/browser/app.js +++ b/lib/browser/app.js @@ -35,6 +35,7 @@ require('./browser/modules/settings'); require('./browser/modules/drive-scanner'); require('./browser/modules/image-writer'); require('./browser/modules/path'); +require('./browser/modules/notifier'); require('./browser/modules/analytics'); const app = angular.module('Etcher', [ @@ -47,6 +48,7 @@ const app = angular.module('Etcher', [ 'Etcher.settings', 'Etcher.drive-scanner', 'Etcher.image-writer', + 'Etcher.notifier', 'Etcher.analytics' ]); @@ -74,6 +76,8 @@ app.config(function($stateProvider, $urlRouterProvider) { app.controller('AppController', function( $q, $state, + $scope, + NotifierService, DriveScannerService, SelectionStateService, ImageWriterService, @@ -86,12 +90,12 @@ app.controller('AppController', function( AnalyticsService.logEvent('Restart'); - if (!this.writer.isBurning()) { - this.state = { - progress: 0, - percentage: 0 - }; - } + NotifierService.subscribe($scope, 'image-writer:state', function(state) { + AnalyticsService.log(`Progress: ${state.progress}% at ${state.speed} MB/s`); + + // Show progress inline in operating system task bar + currentWindow.setProgressBar(state.progress / 100); + }); this.scanner.start(2000).on('scan', function(drives) { @@ -183,14 +187,7 @@ app.controller('AppController', function( device: drive.device }); - return self.writer.burn(image, drive, function(state) { - self.state = state; - AnalyticsService.log(`Progress: ${self.state.progress}% at ${self.state.speed} MB/s`); - - // Show progress inline in operating system task bar - currentWindow.setProgressBar(self.state.progress / 100); - - }).then(function() { + return self.writer.burn(image, drive).then(function() { AnalyticsService.logEvent('Done'); $state.go('success'); }).catch(dialog.showError).finally(function() { @@ -213,11 +210,12 @@ app.controller('NavigationController', function($state) { this.open = shell.openExternal; }); -app.controller('FinishController', function($state, SelectionStateService, SettingsService) { +app.controller('FinishController', function($state, SelectionStateService, SettingsService, ImageWriterService) { this.settings = SettingsService.data; this.restart = function(options) { SelectionStateService.clear(options); + ImageWriterService.resetState(); $state.go('main'); }; }); diff --git a/lib/browser/modules/image-writer.js b/lib/browser/modules/image-writer.js index c87aaec9..84c64cbc 100644 --- a/lib/browser/modules/image-writer.js +++ b/lib/browser/modules/image-writer.js @@ -30,14 +30,39 @@ if (window.mocha) { } require('./settings'); +require('./notifier'); const imageWriter = angular.module('Etcher.image-writer', [ - 'Etcher.settings' + 'Etcher.settings', + 'Etcher.notifier' ]); -imageWriter.service('ImageWriterService', function($q, $timeout, SettingsService) { +imageWriter.service('ImageWriterService', function($q, $timeout, SettingsService, NotifierService) { let self = this; let burning = false; + /** + * @summary Reset burn state + * @function + * @public + * + * @example + * ImageWriterService.resetState(); + */ + this.resetState = function() { + self.state = { + progress: 0, + speed: 0 + }; + }; + + /** + * @summary Burn progress state + * @type Object + * @public + */ + this.state = {}; + this.resetState(); + /** * @summary Check if currently burning * @function @@ -102,11 +127,10 @@ imageWriter.service('ImageWriterService', function($q, $timeout, SettingsService * @public * * @description - * This function will update `state.progress` with the current writing percentage. + * This function will update `ImageWriterService.state` with the current writing state. * * @param {String} image - image path * @param {Object} drive - drive - * @param {Function} onProgress - in progress callback (state) * * @returns {Promise} * @@ -117,7 +141,7 @@ imageWriter.service('ImageWriterService', function($q, $timeout, SettingsService * console.log('Write completed!'); * }); */ - this.burn = function(image, drive, onProgress) { + this.burn = function(image, drive) { if (self.isBurning()) { return $q.reject(new Error('There is already a burn in progress')); } @@ -129,13 +153,14 @@ imageWriter.service('ImageWriterService', function($q, $timeout, SettingsService // Safely bring the state to the world of Angular $timeout(function() { - return onProgress({ + self.state = { progress: Math.floor(state.percentage), // Transform bytes to megabytes preserving only two decimal places speed: Math.floor(state.speed / 1e+6 * 100) / 100 || 0 + }; - }); + NotifierService.emit('image-writer:state', self.state); }); }).finally(function() { diff --git a/lib/browser/modules/notifier.js b/lib/browser/modules/notifier.js new file mode 100644 index 00000000..7295a09f --- /dev/null +++ b/lib/browser/modules/notifier.js @@ -0,0 +1,74 @@ +/* + * Copyright 2016 Resin.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +/** + * @module Etcher.notifier + */ + +const angular = require('angular'); +const notifier = angular.module('Etcher.notifier', []); + +/* + * Based on: + * http://www.codelord.net/2015/05/04/angularjs-notifying-about-changes-from-services-to-controllers/ + */ + +notifier.service('NotifierService', function($rootScope) { + + /** + * @summary Safely subscribe to an event + * @function + * @public + * + * @description + * We say "safely" since this subscribe function will listen + * to the scope's `$destroy` event and unbind itself automatically. + * + * @param {Object} scope - angular scope + * @param {String} name - event name + * @param {Function} callback - callback + * + * @example + * NotifierService.subscribe($scope, 'my-event', function() { + * console.log('Event received!'); + * }); + */ + this.subscribe = function(scope, name, callback) { + const handler = $rootScope.$on(name, function(event, data) { + return callback(data); + }); + + scope.$on('$destroy', handler); + }; + + /** + * @summary Emit an event + * @function + * @public + * + * @param {String} name - event name + * @param {*} data - event data + * + * @example + * NotifierService.emit('my-event', 'Foo'); + */ + this.emit = function(name, data) { + $rootScope.$emit(name, data); + }; + +}); diff --git a/lib/partials/main.html b/lib/partials/main.html index 820d07a6..716f0324 100644 --- a/lib/partials/main.html +++ b/lib/partials/main.html @@ -56,17 +56,17 @@ 3
- - Finishing... - Burn! - Starting... - + Finishing... + Burn! + Starting... + - +
diff --git a/tests/browser/modules/image-writer.spec.js b/tests/browser/modules/image-writer.spec.js index 3bcf4bb2..7748856d 100644 --- a/tests/browser/modules/image-writer.spec.js +++ b/tests/browser/modules/image-writer.spec.js @@ -23,6 +23,35 @@ describe('Browser: ImageWriter', function() { ImageWriterService = _ImageWriterService_; })); + describe('.state', function() { + + it('should be reset by default', function() { + m.chai.expect(ImageWriterService.state).to.deep.equal({ + progress: 0, + speed: 0 + }); + }); + + }); + + describe('.resetState()', function() { + + it('should be able to reset the state', function() { + ImageWriterService.state = { + progress: 50, + speed: 3 + }; + + ImageWriterService.resetState(); + + m.chai.expect(ImageWriterService.state).to.deep.equal({ + progress: 0, + speed: 0 + }); + }); + + }); + describe('.isBurning()', function() { it('should return false by default', function() { diff --git a/tests/browser/modules/notifier.spec.js b/tests/browser/modules/notifier.spec.js new file mode 100644 index 00000000..8af60d63 --- /dev/null +++ b/tests/browser/modules/notifier.spec.js @@ -0,0 +1,50 @@ +'use strict'; + +const m = require('mochainon'); +const angular = require('angular'); +require('angular-mocks'); +require('../../../lib/browser/modules/notifier'); + +describe('Browser: Notifier', function() { + + beforeEach(angular.mock.module('Etcher.notifier')); + + describe('NotifierService', function() { + + let $rootScope; + let NotifierService; + + beforeEach(angular.mock.inject(function(_$rootScope_, _NotifierService_) { + $rootScope = _$rootScope_; + NotifierService = _NotifierService_; + })); + + it('should be able to emit an event without data', function() { + let spy = m.sinon.spy(); + NotifierService.subscribe($rootScope, 'foobar', spy); + NotifierService.emit('foobar'); + m.chai.expect(spy).to.have.been.calledOnce; + m.chai.expect(spy).to.have.been.calledWith(undefined); + }); + + it('should be able to emit an event with data', function() { + let spy = m.sinon.spy(); + NotifierService.subscribe($rootScope, 'foobar', spy); + NotifierService.emit('foobar', 'Hello'); + m.chai.expect(spy).to.have.been.calledOnce; + m.chai.expect(spy).to.have.been.calledWith('Hello'); + }); + + it('should emit the correct event', function() { + let spy1 = m.sinon.spy(); + let spy2 = m.sinon.spy(); + NotifierService.subscribe($rootScope, 'foobar', spy1); + NotifierService.subscribe($rootScope, 'foobaz', spy2); + NotifierService.emit('foobar'); + m.chai.expect(spy1).to.have.been.calledOnce; + m.chai.expect(spy2).to.not.have.been.called; + }); + + }); + +});