mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-25 12:16:37 +00:00
Merge pull request #196 from resin-io/fix/state-progress-closure
Move burn state to ImageWriterService
This commit is contained in:
commit
276e4efc04
@ -35,6 +35,7 @@ require('./browser/modules/settings');
|
|||||||
require('./browser/modules/drive-scanner');
|
require('./browser/modules/drive-scanner');
|
||||||
require('./browser/modules/image-writer');
|
require('./browser/modules/image-writer');
|
||||||
require('./browser/modules/path');
|
require('./browser/modules/path');
|
||||||
|
require('./browser/modules/notifier');
|
||||||
require('./browser/modules/analytics');
|
require('./browser/modules/analytics');
|
||||||
|
|
||||||
const app = angular.module('Etcher', [
|
const app = angular.module('Etcher', [
|
||||||
@ -47,6 +48,7 @@ const app = angular.module('Etcher', [
|
|||||||
'Etcher.settings',
|
'Etcher.settings',
|
||||||
'Etcher.drive-scanner',
|
'Etcher.drive-scanner',
|
||||||
'Etcher.image-writer',
|
'Etcher.image-writer',
|
||||||
|
'Etcher.notifier',
|
||||||
'Etcher.analytics'
|
'Etcher.analytics'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -74,6 +76,8 @@ app.config(function($stateProvider, $urlRouterProvider) {
|
|||||||
app.controller('AppController', function(
|
app.controller('AppController', function(
|
||||||
$q,
|
$q,
|
||||||
$state,
|
$state,
|
||||||
|
$scope,
|
||||||
|
NotifierService,
|
||||||
DriveScannerService,
|
DriveScannerService,
|
||||||
SelectionStateService,
|
SelectionStateService,
|
||||||
ImageWriterService,
|
ImageWriterService,
|
||||||
@ -86,12 +90,12 @@ app.controller('AppController', function(
|
|||||||
|
|
||||||
AnalyticsService.logEvent('Restart');
|
AnalyticsService.logEvent('Restart');
|
||||||
|
|
||||||
if (!this.writer.isBurning()) {
|
NotifierService.subscribe($scope, 'image-writer:state', function(state) {
|
||||||
this.state = {
|
AnalyticsService.log(`Progress: ${state.progress}% at ${state.speed} MB/s`);
|
||||||
progress: 0,
|
|
||||||
percentage: 0
|
// Show progress inline in operating system task bar
|
||||||
};
|
currentWindow.setProgressBar(state.progress / 100);
|
||||||
}
|
});
|
||||||
|
|
||||||
this.scanner.start(2000).on('scan', function(drives) {
|
this.scanner.start(2000).on('scan', function(drives) {
|
||||||
|
|
||||||
@ -183,14 +187,7 @@ app.controller('AppController', function(
|
|||||||
device: drive.device
|
device: drive.device
|
||||||
});
|
});
|
||||||
|
|
||||||
return self.writer.burn(image, drive, function(state) {
|
return self.writer.burn(image, drive).then(function() {
|
||||||
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() {
|
|
||||||
AnalyticsService.logEvent('Done');
|
AnalyticsService.logEvent('Done');
|
||||||
$state.go('success');
|
$state.go('success');
|
||||||
}).catch(dialog.showError).finally(function() {
|
}).catch(dialog.showError).finally(function() {
|
||||||
@ -213,11 +210,12 @@ app.controller('NavigationController', function($state) {
|
|||||||
this.open = shell.openExternal;
|
this.open = shell.openExternal;
|
||||||
});
|
});
|
||||||
|
|
||||||
app.controller('FinishController', function($state, SelectionStateService, SettingsService) {
|
app.controller('FinishController', function($state, SelectionStateService, SettingsService, ImageWriterService) {
|
||||||
this.settings = SettingsService.data;
|
this.settings = SettingsService.data;
|
||||||
|
|
||||||
this.restart = function(options) {
|
this.restart = function(options) {
|
||||||
SelectionStateService.clear(options);
|
SelectionStateService.clear(options);
|
||||||
|
ImageWriterService.resetState();
|
||||||
$state.go('main');
|
$state.go('main');
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -30,14 +30,39 @@ if (window.mocha) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
require('./settings');
|
require('./settings');
|
||||||
|
require('./notifier');
|
||||||
const imageWriter = angular.module('Etcher.image-writer', [
|
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 self = this;
|
||||||
let burning = false;
|
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
|
* @summary Check if currently burning
|
||||||
* @function
|
* @function
|
||||||
@ -102,11 +127,10 @@ imageWriter.service('ImageWriterService', function($q, $timeout, SettingsService
|
|||||||
* @public
|
* @public
|
||||||
*
|
*
|
||||||
* @description
|
* @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 {String} image - image path
|
||||||
* @param {Object} drive - drive
|
* @param {Object} drive - drive
|
||||||
* @param {Function} onProgress - in progress callback (state)
|
|
||||||
*
|
*
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*
|
*
|
||||||
@ -117,7 +141,7 @@ imageWriter.service('ImageWriterService', function($q, $timeout, SettingsService
|
|||||||
* console.log('Write completed!');
|
* console.log('Write completed!');
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
this.burn = function(image, drive, onProgress) {
|
this.burn = function(image, drive) {
|
||||||
if (self.isBurning()) {
|
if (self.isBurning()) {
|
||||||
return $q.reject(new Error('There is already a burn in progress'));
|
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
|
// Safely bring the state to the world of Angular
|
||||||
$timeout(function() {
|
$timeout(function() {
|
||||||
|
|
||||||
return onProgress({
|
self.state = {
|
||||||
progress: Math.floor(state.percentage),
|
progress: Math.floor(state.percentage),
|
||||||
|
|
||||||
// Transform bytes to megabytes preserving only two decimal places
|
// Transform bytes to megabytes preserving only two decimal places
|
||||||
speed: Math.floor(state.speed / 1e+6 * 100) / 100 || 0
|
speed: Math.floor(state.speed / 1e+6 * 100) / 100 || 0
|
||||||
|
};
|
||||||
|
|
||||||
});
|
NotifierService.emit('image-writer:state', self.state);
|
||||||
});
|
});
|
||||||
|
|
||||||
}).finally(function() {
|
}).finally(function() {
|
||||||
|
74
lib/browser/modules/notifier.js
Normal file
74
lib/browser/modules/notifier.js
Normal 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
@ -56,17 +56,17 @@
|
|||||||
<hero-badge class="block space-vertical-medium" ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()">3</hero-badge>
|
<hero-badge class="block space-vertical-medium" ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()">3</hero-badge>
|
||||||
|
|
||||||
<div class="space-vertical-large">
|
<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-click="app.burn(app.selection.getImage(), app.selection.getDrive())"
|
||||||
ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()">
|
ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()">
|
||||||
<span ng-show="app.state.progress == 100 && app.writer.isBurning()">Finishing...</span>
|
<span ng-show="app.writer.state.progress == 100 && app.writer.isBurning()">Finishing...</span>
|
||||||
<span ng-show="app.state.progress == 0 && !app.writer.isBurning()">Burn!</span>
|
<span ng-show="app.writer.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.writer.state.progress == 0 && app.writer.isBurning() && !app.writer.state.speed">Starting...</span>
|
||||||
<span ng-show="app.state.speed && app.state.progress != 100"
|
<span ng-show="app.writer.state.speed && app.writer.state.progress != 100"
|
||||||
ng-bind="app.state.progress + '% '"></span>
|
ng-bind="app.writer.state.progress + '% '"></span>
|
||||||
</hero-progress-button>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,6 +23,35 @@ describe('Browser: ImageWriter', function() {
|
|||||||
ImageWriterService = _ImageWriterService_;
|
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() {
|
describe('.isBurning()', function() {
|
||||||
|
|
||||||
it('should return false by default', function() {
|
it('should return false by default', function() {
|
||||||
|
50
tests/browser/modules/notifier.spec.js
Normal file
50
tests/browser/modules/notifier.spec.js
Normal 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;
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user