Merge pull request #196 from resin-io/fix/state-progress-closure

Move burn state to ImageWriterService
This commit is contained in:
Juan Cruz Viotti 2016-03-10 12:11:47 -04:00
commit 276e4efc04
6 changed files with 205 additions and 29 deletions

View File

@ -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');
};
});

View File

@ -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() {

View File

@ -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);
};
});

View File

@ -56,17 +56,17 @@
<hero-badge class="block space-vertical-medium" ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()">3</hero-badge>
<div class="space-vertical-large">
<hero-progress-button percentage="{{ app.state.progress }}" ng-attr-active="{{ app.writer.isBurning() }}"
<hero-progress-button percentage="{{ app.writer.state.progress }}" ng-attr-active="{{ app.writer.isBurning() }}"
ng-click="app.burn(app.selection.getImage(), app.selection.getDrive())"
ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()">
<span ng-show="app.state.progress == 100 && app.writer.isBurning()">Finishing...</span>
<span ng-show="app.state.progress == 0 && !app.writer.isBurning()">Burn!</span>
<span ng-show="app.state.progress == 0 && app.writer.isBurning() && !app.state.speed">Starting...</span>
<span ng-show="app.state.speed && app.state.progress != 100"
ng-bind="app.state.progress + '% '"></span>
<span ng-show="app.writer.state.progress == 100 && app.writer.isBurning()">Finishing...</span>
<span ng-show="app.writer.state.progress == 0 && !app.writer.isBurning()">Burn!</span>
<span ng-show="app.writer.state.progress == 0 && app.writer.isBurning() && !app.writer.state.speed">Starting...</span>
<span ng-show="app.writer.state.speed && app.writer.state.progress != 100"
ng-bind="app.writer.state.progress + '% '"></span>
</hero-progress-button>
<p class="step-footer" ng-bind="app.state.speed.toFixed(2) + ' MB/s'" ng-show="app.state.speed && app.state.progress != 100"></p>
<p class="step-footer" ng-bind="app.writer.state.speed.toFixed(2) + ' MB/s'" ng-show="app.writer.state.speed && app.writer.state.progress != 100"></p>
</div>
</div>
</div>

View File

@ -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() {

View File

@ -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;
});
});
});