feat(GUI): add progress and status to window title (#1438)

* feat(GUI): add progress and status to window title

We add the progress percentage to the window alongside the status
(validating, flashing).

Closes: https://github.com/resin-io/etcher/issues/1427
Fixes: https://github.com/resin-io/etcher/issues/1439
Changelog-Entry: Add the progress and status to the window title.
This commit is contained in:
Benedict Aas 2017-11-14 09:23:41 +00:00 committed by GitHub
parent 5771c0f56e
commit 0058919a8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 289 additions and 258 deletions

View File

@ -179,6 +179,10 @@ app.run(() => {
app.run(() => {
store.subscribe(() => {
if (!flashState.isFlashing()) {
return
}
const currentFlashState = flashState.getFlashState()
// There is usually a short time period between the `isFlashing()`
@ -188,17 +192,15 @@ app.run(() => {
//
// We use the presence of `.eta` to determine that the actual
// writing started.
if (!flashState.isFlashing() || !currentFlashState.eta) {
return
if (!currentFlashState.eta) {
analytics.logDebug(
`Progress (${currentFlashState.type}): ` +
`${currentFlashState.percentage}% at ${currentFlashState.speed} MB/s ` +
`(eta ${currentFlashState.eta}s)`
)
}
analytics.logDebug([
`Progress (${currentFlashState.type}):`,
`${currentFlashState.percentage}% at ${currentFlashState.speed} MB/s`,
`(eta ${currentFlashState.eta}s)`
].join(' '))
windowProgress.set(currentFlashState.percentage)
windowProgress.set(currentFlashState)
})
})

View File

@ -0,0 +1,63 @@
/*
* Copyright 2017 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'
const settings = require('../models/settings')
const utils = require('../../shared/utils')
/**
* @summary Make the progress status subtitle string
*
* @param {Object} state - flashing metadata
*
* @returns {String}
*
* @example
* const status = progressStatus.fromFlashState({
* type: 'write',
* percentage: 55,
* speed: 2049
* })
*
* console.log(status)
* // '55% Flashing'
*/
exports.fromFlashState = (state) => {
const isChecking = state.type === 'check'
const shouldValidate = settings.get('validateWriteOnSuccess')
const shouldUnmount = settings.get('unmountOnSuccess')
if (state.percentage === utils.PERCENTAGE_MINIMUM && !state.speed) {
if (isChecking) {
return 'Validating...'
}
return 'Starting...'
} else if (state.percentage === utils.PERCENTAGE_MAXIMUM) {
if ((isChecking || !shouldValidate) && shouldUnmount) {
return 'Unmounting...'
}
return 'Finishing...'
} else if (state.type === 'write') {
return `${state.percentage}% Flashing`
} else if (state.type === 'check') {
return `${state.percentage}% Validating`
}
throw new Error(`Invalid state: ${JSON.stringify(state)}`)
}

View File

@ -18,6 +18,43 @@
const electron = require('electron')
const utils = require('../../shared/utils')
const progressStatus = require('../modules/progress-status')
/**
* @summary The title of the main window upon program launch
* @type {String}
* @private
* @constant
*/
const INITIAL_TITLE = document.title
/**
* @summary Make the full window status title
* @private
*
* @param {Object} state - flash state object
*
* @returns {String}
*
* @example
* const title = getWindowTitle({
* type: 'write',
* percentage: 55,
* speed: 2049
* });
*
* console.log(title);
* // 'Etcher \u2013 55% Flashing'
*/
const getWindowTitle = (state) => {
if (state) {
const subtitle = progressStatus.fromFlashState(state)
const DASH_UNICODE_CHAR = '\u2013'
return `${INITIAL_TITLE} ${DASH_UNICODE_CHAR} ${subtitle}`
}
return INITIAL_TITLE
}
/**
* @summary A reference to the current renderer Electron window
@ -37,13 +74,18 @@ exports.currentWindow = electron.remote.getCurrentWindow()
* @description
* Show progress inline in operating system task bar
*
* @param {Number} percentage - percentage
* @param {Number} state - flash state object
*
* @example
* windowProgress.set(85);
* windowProgress.set({
* type: 'write',
* percentage: 55,
* speed: 2049
* })
*/
exports.set = (percentage) => {
exports.currentWindow.setProgressBar(utils.percentageToFloat(percentage))
exports.set = (state) => {
exports.currentWindow.setProgressBar(utils.percentageToFloat(state.percentage))
exports.currentWindow.setTitle(getWindowTitle(state))
}
/**
@ -59,4 +101,5 @@ exports.clear = () => {
const ELECTRON_PROGRESS_BAR_RESET_VALUE = -1
exports.currentWindow.setProgressBar(ELECTRON_PROGRESS_BAR_RESET_VALUE)
exports.currentWindow.setTitle(getWindowTitle(null))
}

View File

@ -17,10 +17,9 @@
'use strict'
const messages = require('../../../../shared/messages')
const settings = require('../../../models/settings')
const flashState = require('../../../../shared/models/flash-state')
const driveScanner = require('../../../modules/drive-scanner')
const utils = require('../../../../shared/utils')
const progressStatus = require('../../../modules/progress-status')
const notification = require('../../../os/notification')
const exceptionReporter = require('../../../modules/exception-reporter')
const path = require('path')
@ -119,23 +118,10 @@ module.exports = function (
* const label = FlashController.getProgressButtonLabel();
*/
this.getProgressButtonLabel = () => {
const currentFlashState = flashState.getFlashState()
const isChecking = currentFlashState.type === 'check'
if (!flashState.isFlashing()) {
return 'Flash!'
} else if (currentFlashState.percentage === utils.PERCENTAGE_MINIMUM && !currentFlashState.speed) {
return 'Starting...'
} else if (currentFlashState.percentage === utils.PERCENTAGE_MAXIMUM) {
if (isChecking && settings.get('unmountOnSuccess')) {
return 'Unmounting...'
}
return 'Finishing...'
} else if (isChecking) {
return `${currentFlashState.percentage}% Validating...`
}
return `${currentFlashState.percentage}%`
return progressStatus.fromFlashState(flashState.getFlashState())
}
}

View File

@ -0,0 +1,104 @@
'use strict'
const m = require('mochainon')
const settings = require('../../../lib/gui/models/settings')
const progressStatus = require('../../../lib/gui/modules/progress-status')
describe('Browser: progressStatus', function () {
describe('.fromFlashState()', function () {
beforeEach(function () {
this.state = {
type: 'write',
percentage: 0,
eta: 15,
speed: 100000000000000
}
settings.set('unmountOnSuccess', true)
settings.set('validateWriteOnSuccess', true)
})
it('should report 0% if percentage == 0 but speed != 0', function () {
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('0% Flashing')
})
it('should handle percentage == 0, type == write, unmountOnSuccess', function () {
this.state.speed = 0
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Starting...')
})
it('should handle percentage == 0, type == write, !unmountOnSuccess', function () {
this.state.speed = 0
settings.set('unmountOnSuccess', false)
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Starting...')
})
it('should handle percentage == 0, type == check, unmountOnSuccess', function () {
this.state.speed = 0
this.state.type = 'check'
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Validating...')
})
it('should handle percentage == 0, type == check, !unmountOnSuccess', function () {
this.state.speed = 0
this.state.type = 'check'
settings.set('unmountOnSuccess', false)
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Validating...')
})
it('should handle percentage == 50, type == write, unmountOnSuccess', function () {
this.state.percentage = 50
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('50% Flashing')
})
it('should handle percentage == 50, type == write, !unmountOnSuccess', function () {
this.state.percentage = 50
settings.set('unmountOnSuccess', false)
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('50% Flashing')
})
it('should handle percentage == 50, type == check, unmountOnSuccess', function () {
this.state.type = 'check'
this.state.percentage = 50
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('50% Validating')
})
it('should handle percentage == 50, type == check, !unmountOnSuccess', function () {
this.state.type = 'check'
this.state.percentage = 50
settings.set('unmountOnSuccess', false)
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('50% Validating')
})
it('should handle percentage == 100, type == write, unmountOnSuccess, validateWriteOnSuccess', function () {
this.state.percentage = 100
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Finishing...')
})
it('should handle percentage == 100, type == write, unmountOnSuccess, !validateWriteOnSuccess', function () {
this.state.percentage = 100
settings.set('validateWriteOnSuccess', false)
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Unmounting...')
})
it('should handle percentage == 100, type == write, !unmountOnSuccess, !validateWriteOnSuccess', function () {
this.state.percentage = 100
settings.set('unmountOnSuccess', false)
settings.set('validateWriteOnSuccess', false)
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Finishing...')
})
it('should handle percentage == 100, type == check, unmountOnSuccess', function () {
this.state.type = 'check'
this.state.percentage = 100
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Unmounting...')
})
it('should handle percentage == 100, type == check, !unmountOnSuccess', function () {
this.state.type = 'check'
this.state.percentage = 100
settings.set('unmountOnSuccess', false)
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Finishing...')
})
})
})

View File

@ -24,39 +24,80 @@ describe('Browser: WindowProgress', function () {
describe('given a stubbed current window', function () {
beforeEach(function () {
this.setProgressBarSpy = m.sinon.spy()
this.setTitleSpy = m.sinon.spy()
windowProgress.currentWindow = {
setProgressBar: this.setProgressBarSpy
setProgressBar: this.setProgressBarSpy,
setTitle: this.setTitleSpy
}
this.state = {
percentage: 85,
speed: 100,
type: 'write'
}
})
describe('.set()', function () {
it('should translate 0-100 percentages to 0-1 ranges', function () {
windowProgress.set(85)
windowProgress.set(this.state)
m.chai.expect(this.setProgressBarSpy).to.have.been.calledWith(0.85)
})
it('should set 0 given 0', function () {
windowProgress.set(0)
this.state.percentage = 0
windowProgress.set(this.state)
m.chai.expect(this.setProgressBarSpy).to.have.been.calledWith(0)
})
it('should set 1 given 100', function () {
windowProgress.set(100)
this.state.percentage = 100
windowProgress.set(this.state)
m.chai.expect(this.setProgressBarSpy).to.have.been.calledWith(1)
})
it('should throw if given a percentage higher than 100', function () {
this.state.percentage = 101
const state = this.state
m.chai.expect(function () {
windowProgress.set(101)
windowProgress.set(state)
}).to.throw('Invalid percentage: 101')
})
it('should throw if given a percentage less than 0', function () {
this.state.percentage = -1
const state = this.state
m.chai.expect(function () {
windowProgress.set(-1)
windowProgress.set(state)
}).to.throw('Invalid percentage: -1')
})
it('should set the flashing title', function () {
this.state.type = 'write'
windowProgress.set(this.state)
m.chai.expect(this.setTitleSpy).to.have.been.calledWith(' \u2013 85% Flashing')
})
it('should set the validating title', function () {
this.state.type = 'check'
windowProgress.set(this.state)
m.chai.expect(this.setTitleSpy).to.have.been.calledWith(' \u2013 85% Validating')
})
it('should set the starting title', function () {
this.state.type = 'write'
this.state.percentage = 0
this.state.speed = 0
windowProgress.set(this.state)
m.chai.expect(this.setTitleSpy).to.have.been.calledWith(' \u2013 Starting...')
})
it('should set the finishing title', function () {
this.state.type = 'write'
this.state.percentage = 100
windowProgress.set(this.state)
m.chai.expect(this.setTitleSpy).to.have.been.calledWith(' \u2013 Finishing...')
})
})
describe('.clear()', function () {
@ -64,6 +105,11 @@ describe('Browser: WindowProgress', function () {
windowProgress.clear()
m.chai.expect(this.setProgressBarSpy).to.have.been.calledWith(-1)
})
it('should clear the window title', function () {
windowProgress.clear()
m.chai.expect(this.setTitleSpy).to.have.been.calledWith('')
})
})
})
})

View File

@ -21,7 +21,6 @@ const _ = require('lodash')
const path = require('path')
const supportedFormats = require('../../../lib/shared/supported-formats')
const angular = require('angular')
const settings = require('../../../lib/gui/models/settings')
const flashState = require('../../../lib/shared/models/flash-state')
const availableDrives = require('../../../lib/shared/models/available-drives')
const selectionState = require('../../../lib/shared/models/selection-state')
@ -226,231 +225,19 @@ describe('Browser: MainPage', function () {
m.chai.expect(controller.getProgressButtonLabel()).to.equal('Flash!')
})
describe('given there is a flash in progress', function () {
beforeEach(function () {
flashState.setFlashingFlag()
it('should display the flashing progress', function () {
const controller = $controller('FlashController', {
$scope: {}
})
it('should report 0% if percentage == 0 but speed != 0', function () {
const controller = $controller('FlashController', {
$scope: {}
})
flashState.setProgressState({
type: 'write',
percentage: 0,
eta: 15,
speed: 100000000000000
})
return settings.set('unmountOnSuccess', true).then(() => {
m.chai.expect(controller.getProgressButtonLabel()).to.equal('0%')
})
})
it('should handle percentage == 0, type = write, unmountOnSuccess', function () {
const controller = $controller('FlashController', {
$scope: {}
})
flashState.setProgressState({
type: 'write',
percentage: 0,
eta: 15,
speed: 0
})
return settings.set('unmountOnSuccess', true).then(() => {
m.chai.expect(controller.getProgressButtonLabel()).to.equal('Starting...')
})
})
it('should handle percentage == 0, type = write, !unmountOnSuccess', function () {
const controller = $controller('FlashController', {
$scope: {}
})
flashState.setProgressState({
type: 'write',
percentage: 0,
eta: 15,
speed: 0
})
return settings.set('unmountOnSuccess', false).then(() => {
m.chai.expect(controller.getProgressButtonLabel()).to.equal('Starting...')
})
})
it('should handle percentage == 0, type = check, unmountOnSuccess', function () {
const controller = $controller('FlashController', {
$scope: {}
})
flashState.setProgressState({
type: 'check',
percentage: 0,
eta: 15,
speed: 0
})
return settings.set('unmountOnSuccess', true).then(() => {
m.chai.expect(controller.getProgressButtonLabel()).to.equal('Starting...')
})
})
it('should handle percentage == 0, type = check, !unmountOnSuccess', function () {
const controller = $controller('FlashController', {
$scope: {}
})
flashState.setProgressState({
type: 'check',
percentage: 0,
eta: 15,
speed: 0
})
return settings.set('unmountOnSuccess', false).then(() => {
m.chai.expect(controller.getProgressButtonLabel()).to.equal('Starting...')
})
})
it('should handle percentage == 50, type = write, unmountOnSuccess', function () {
const controller = $controller('FlashController', {
$scope: {}
})
flashState.setProgressState({
type: 'write',
percentage: 50,
eta: 15,
speed: 1000
})
return settings.set('unmountOnSuccess', true).then(() => {
m.chai.expect(controller.getProgressButtonLabel()).to.equal('50%')
})
})
it('should handle percentage == 50, type = write, !unmountOnSuccess', function () {
const controller = $controller('FlashController', {
$scope: {}
})
flashState.setProgressState({
type: 'write',
percentage: 50,
eta: 15,
speed: 1000
})
return settings.set('unmountOnSuccess', false).then(() => {
m.chai.expect(controller.getProgressButtonLabel()).to.equal('50%')
})
})
it('should handle percentage == 50, type = check, unmountOnSuccess', function () {
const controller = $controller('FlashController', {
$scope: {}
})
flashState.setProgressState({
type: 'check',
percentage: 50,
eta: 15,
speed: 1000
})
return settings.set('unmountOnSuccess', true).then(() => {
m.chai.expect(controller.getProgressButtonLabel()).to.equal('50% Validating...')
})
})
it('should handle percentage == 50, type = check, !unmountOnSuccess', function () {
const controller = $controller('FlashController', {
$scope: {}
})
flashState.setProgressState({
type: 'check',
percentage: 50,
eta: 15,
speed: 1000
})
return settings.set('unmountOnSuccess', false).then(() => {
m.chai.expect(controller.getProgressButtonLabel()).to.equal('50% Validating...')
})
})
it('should handle percentage == 100, type = write, unmountOnSuccess', function () {
const controller = $controller('FlashController', {
$scope: {}
})
flashState.setProgressState({
type: 'write',
percentage: 100,
eta: 15,
speed: 1000
})
return settings.set('unmountOnSuccess', true).then(() => {
m.chai.expect(controller.getProgressButtonLabel()).to.equal('Finishing...')
})
})
it('should handle percentage == 100, type = write, !unmountOnSuccess', function () {
const controller = $controller('FlashController', {
$scope: {}
})
flashState.setProgressState({
type: 'write',
percentage: 100,
eta: 15,
speed: 1000
})
return settings.set('unmountOnSuccess', false).then(() => {
m.chai.expect(controller.getProgressButtonLabel()).to.equal('Finishing...')
})
})
it('should handle percentage == 100, type = check, unmountOnSuccess', function () {
const controller = $controller('FlashController', {
$scope: {}
})
flashState.setProgressState({
type: 'check',
percentage: 100,
eta: 15,
speed: 1000
})
return settings.set('unmountOnSuccess', true).then(() => {
m.chai.expect(controller.getProgressButtonLabel()).to.equal('Unmounting...')
})
})
it('should handle percentage == 100, type = check, !unmountOnSuccess', function () {
const controller = $controller('FlashController', {
$scope: {}
})
flashState.setProgressState({
type: 'check',
percentage: 100,
eta: 15,
speed: 1000
})
return settings.set('unmountOnSuccess', false).then(() => {
m.chai.expect(controller.getProgressButtonLabel()).to.equal('Finishing...')
})
flashState.setFlashingFlag()
flashState.setProgressState({
type: 'write',
percentage: 85,
eta: 15,
speed: 1000
})
m.chai.expect(controller.getProgressButtonLabel()).to.equal('85% Flashing')
})
})
})