diff --git a/lib/gui/app.js b/lib/gui/app.js index 38035e2e..166f07ce 100644 --- a/lib/gui/app.js +++ b/lib/gui/app.js @@ -43,15 +43,14 @@ const updateNotifier = require('./components/update-notifier'); const availableDrives = require('./models/available-drives'); const selectionState = require('./models/selection-state'); const driveScanner = require('./modules/drive-scanner'); +const osDialog = require('./os/dialog'); +const exceptionReporter = require('./modules/exception-reporter'); const app = angular.module('Etcher', [ require('angular-ui-router'), require('angular-ui-bootstrap'), require('angular-if-state'), - // Etcher modules - require('./modules/error'), - // Components require('./components/svg-icon'), require('./components/warning-modal/warning-modal'), @@ -65,7 +64,6 @@ const app = angular.module('Etcher', [ // OS require('./os/open-external/open-external'), require('./os/dropzone/dropzone'), - require('./os/dialog/dialog'), // Utils require('./utils/manifest-bind/manifest-bind') @@ -85,7 +83,7 @@ app.run(() => { ].join('\n')); }); -app.run((ErrorService) => { +app.run(() => { analytics.logEvent('Application start'); settings.load(); @@ -148,7 +146,7 @@ app.run((ErrorService) => { return updateNotifier.notify(latestVersion, { allowSleepUpdateCheck: currentReleaseType === release.RELEASE_TYPE.PRODUCTION }); - }).catch(ErrorService.reportException); + }).catch(exceptionReporter.report); }); app.run(() => { @@ -176,7 +174,7 @@ app.run(() => { }); }); -app.run(($timeout, ErrorService) => { +app.run(($timeout) => { driveScanner.on('drives', (drives) => { // Safely trigger a digest cycle. @@ -196,13 +194,13 @@ app.run(($timeout, ErrorService) => { // spamming our error reporting service. driveScanner.stop(); - return ErrorService.reportException(error); + return exceptionReporter.report(error); }); driveScanner.start(); }); -app.run(($window, WarningModalService, ErrorService, OSDialogService) => { +app.run(($window) => { let popupExists = false; $window.addEventListener('beforeunload', (event) => { @@ -221,7 +219,7 @@ app.run(($window, WarningModalService, ErrorService, OSDialogService) => { analytics.logEvent('Close attempt while flashing'); - OSDialogService.showWarning({ + osDialog.showWarning({ confirmationLabel: 'Yes, quit', rejectionLabel: 'Cancel', title: 'Are you sure you want to close Etcher?', @@ -240,7 +238,7 @@ app.run(($window, WarningModalService, ErrorService, OSDialogService) => { analytics.logEvent('Close rejected while flashing'); popupExists = false; - }).catch(ErrorService.reportException); + }).catch(exceptionReporter.report); }); }); @@ -264,10 +262,9 @@ app.config(($urlRouterProvider) => { }); app.config(($provide) => { - $provide.decorator('$exceptionHandler', ($delegate, $injector) => { + $provide.decorator('$exceptionHandler', ($delegate) => { return (exception, cause) => { - const ErrorService = $injector.get('ErrorService'); - ErrorService.reportException(exception); + exceptionReporter.report(exception); $delegate(exception, cause); }; }); diff --git a/lib/gui/modules/error.js b/lib/gui/modules/error.js deleted file mode 100644 index 98f538e1..00000000 --- a/lib/gui/modules/error.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.Modules.Error - */ - -const angular = require('angular'); -const _ = require('lodash'); -const analytics = require('../modules/analytics'); - -const MODULE_NAME = 'Etcher.Modules.Error'; -const error = angular.module(MODULE_NAME, [ - require('../os/dialog/dialog') -]); - -error.service('ErrorService', function(OSDialogService) { - - /** - * @summary Report an exception - * @function - * @public - * - * @param {Error} exception - exception - * - * @example - * ErrorService.reportException(new Error('Something happened')); - */ - this.reportException = (exception) => { - if (_.isUndefined(exception)) { - return; - } - - OSDialogService.showError(exception); - analytics.logException(exception); - }; - -}); - -module.exports = MODULE_NAME; diff --git a/lib/gui/os/dialog/dialog.js b/lib/gui/modules/exception-reporter.js similarity index 57% rename from lib/gui/os/dialog/dialog.js rename to lib/gui/modules/exception-reporter.js index 531a36d7..c170a329 100644 --- a/lib/gui/os/dialog/dialog.js +++ b/lib/gui/modules/exception-reporter.js @@ -16,17 +16,25 @@ 'use strict'; +const _ = require('lodash'); +const analytics = require('../modules/analytics'); +const osDialog = require('../os/dialog'); + /** - * The purpose of this module is to provide an easy way - * to interact with OS dialogs. + * @summary Report an exception + * @function + * @public * - * @module Etcher.OS.Dialog + * @param {Error} exception - exception + * + * @example + * exceptionReporter.report(new Error('Something happened')); */ +exports.report = (exception) => { + if (_.isUndefined(exception)) { + return; + } -const angular = require('angular'); -const MODULE_NAME = 'Etcher.OS.Dialog'; -const OSDialog = angular.module(MODULE_NAME, []); - -OSDialog.service('OSDialogService', require('./services/dialog')); - -module.exports = MODULE_NAME; + osDialog.showError(exception); + analytics.logException(exception); +}; diff --git a/lib/gui/os/dialog.js b/lib/gui/os/dialog.js new file mode 100644 index 00000000..d922b95a --- /dev/null +++ b/lib/gui/os/dialog.js @@ -0,0 +1,149 @@ +/* + * 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'; + +const _ = require('lodash'); +const electron = require('electron'); +const Bluebird = require('bluebird'); +const errors = require('../../shared/errors'); +const supportedFormats = require('../../shared/supported-formats'); + +/** + * @summary Current renderer BrowserWindow instance + * @type {Object} + * @private + */ +const currentWindow = electron.remote.getCurrentWindow(); + +/** + * @summary Open an image selection dialog + * @function + * @public + * + * @description + * Notice that by image, we mean *.img/*.iso/*.zip/etc files. + * + * @fulfil {Object} - selected image + * @returns {Promise}; + * + * @example + * osDialog.selectImage().then((image) => { + * console.log('The selected image is', image.path); + * }); + */ +exports.selectImage = () => { + return new Bluebird((resolve) => { + electron.remote.dialog.showOpenDialog(currentWindow, { + + // This variable is set when running in GNU/Linux from + // inside an AppImage, and represents the working directory + // from where the AppImage was run (which might not be the + // place where the AppImage is located). `OWD` stands for + // "Original Working Directory". + // + // See: https://github.com/probonopd/AppImageKit/commit/1569d6f8540aa6c2c618dbdb5d6fcbf0003952b7 + defaultPath: process.env.OWD, + + properties: [ + 'openFile' + ], + filters: [ + { + name: 'OS Images', + extensions: _.sortBy(supportedFormats.getAllExtensions()) + } + ] + }, (files) => { + + // `_.first` is smart enough to not throw and return `undefined` + // if we pass it an `undefined` value (e.g: when the selection + // dialog was cancelled). + return resolve(_.first(files)); + + }); + }); +}; + +/** + * @summary Open a warning dialog + * @function + * @public + * + * @param {Object} options - options + * @param {String} options.title - dialog title + * @param {String} options.description - dialog description + * @param {String} [options.confirmationLabel="OK"] - confirmation label + * @param {String} [options.rejectionLabel="Cancel"] - rejection label + * @fulfil {Boolean} - whether the dialog was confirmed or not + * @returns {Promise}; + * + * @example + * osDialog.showWarning({ + * title: 'This is a warning', + * description: 'Are you sure you want to continue?', + * confirmationLabel: 'Yes, continue', + * rejectionLabel: 'Cancel' + * }).then((confirmed) => { + * if (confirmed) { + * console.log('The dialog was confirmed'); + * } + * }); + */ +exports.showWarning = (options) => { + _.defaults(options, { + confirmationLabel: 'OK', + rejectionLabel: 'Cancel' + }); + + const BUTTONS = [ + options.confirmationLabel, + options.rejectionLabel + ]; + + const BUTTON_CONFIRMATION_INDEX = _.indexOf(BUTTONS, options.confirmationLabel); + const BUTTON_REJECTION_INDEX = _.indexOf(BUTTONS, options.rejectionLabel); + + return new Bluebird((resolve) => { + electron.remote.dialog.showMessageBox(currentWindow, { + type: 'warning', + buttons: BUTTONS, + defaultId: BUTTON_REJECTION_INDEX, + cancelId: BUTTON_REJECTION_INDEX, + title: 'Attention', + message: options.title, + detail: options.description + }, (response) => { + return resolve(response === BUTTON_CONFIRMATION_INDEX); + }); + }); +}; + +/** + * @summary Show error dialog for an Error instance + * @function + * @public + * + * @param {Error} error - error + * + * @example + * osDialog.showError(new Error('Foo Bar')); + */ +exports.showError = (error) => { + const title = errors.getTitle(error); + const message = errors.getDescription(error); + electron.remote.dialog.showErrorBox(title, message); +}; diff --git a/lib/gui/os/dialog/services/dialog.js b/lib/gui/os/dialog/services/dialog.js deleted file mode 100644 index c8ede599..00000000 --- a/lib/gui/os/dialog/services/dialog.js +++ /dev/null @@ -1,152 +0,0 @@ -/* - * 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'; - -const _ = require('lodash'); -const electron = require('electron'); -const errors = require('../../../../shared/errors'); -const supportedFormats = require('../../../../shared/supported-formats'); - -module.exports = function($q) { - - /** - * @summary Current renderer BrowserWindow instance - * @type {Object} - * @private - */ - const currentWindow = electron.remote.getCurrentWindow(); - - /** - * @summary Open an image selection dialog - * @function - * @public - * - * @description - * Notice that by image, we mean *.img/*.iso/*.zip/etc files. - * - * @fulfil {Object} - selected image - * @returns {Promise}; - * - * @example - * OSDialogService.selectImage().then((image) => { - * console.log('The selected image is', image.path); - * }); - */ - this.selectImage = () => { - return $q((resolve) => { - electron.remote.dialog.showOpenDialog(currentWindow, { - - // This variable is set when running in GNU/Linux from - // inside an AppImage, and represents the working directory - // from where the AppImage was run (which might not be the - // place where the AppImage is located). `OWD` stands for - // "Original Working Directory". - // - // See: https://github.com/probonopd/AppImageKit/commit/1569d6f8540aa6c2c618dbdb5d6fcbf0003952b7 - defaultPath: process.env.OWD, - - properties: [ - 'openFile' - ], - filters: [ - { - name: 'OS Images', - extensions: _.sortBy(supportedFormats.getAllExtensions()) - } - ] - }, (files) => { - - // `_.first` is smart enough to not throw and return `undefined` - // if we pass it an `undefined` value (e.g: when the selection - // dialog was cancelled). - return resolve(_.first(files)); - - }); - }); - }; - - /** - * @summary Open a warning dialog - * @function - * @public - * - * @param {Object} options - options - * @param {String} options.title - dialog title - * @param {String} options.description - dialog description - * @param {String} [options.confirmationLabel="OK"] - confirmation label - * @param {String} [options.rejectionLabel="Cancel"] - rejection label - * @fulfil {Boolean} - whether the dialog was confirmed or not - * @returns {Promise}; - * - * @example - * OSDialogService.showWarning({ - * title: 'This is a warning', - * description: 'Are you sure you want to continue?', - * confirmationLabel: 'Yes, continue', - * rejectionLabel: 'Cancel' - * }).then((confirmed) => { - * if (confirmed) { - * console.log('The dialog was confirmed'); - * } - * }); - */ - this.showWarning = (options) => { - _.defaults(options, { - confirmationLabel: 'OK', - rejectionLabel: 'Cancel' - }); - - const BUTTONS = [ - options.confirmationLabel, - options.rejectionLabel - ]; - - const BUTTON_CONFIRMATION_INDEX = _.indexOf(BUTTONS, options.confirmationLabel); - const BUTTON_REJECTION_INDEX = _.indexOf(BUTTONS, options.rejectionLabel); - - return $q((resolve) => { - electron.remote.dialog.showMessageBox(currentWindow, { - type: 'warning', - buttons: BUTTONS, - defaultId: BUTTON_REJECTION_INDEX, - cancelId: BUTTON_REJECTION_INDEX, - title: 'Attention', - message: options.title, - detail: options.description - }, (response) => { - return resolve(response === BUTTON_CONFIRMATION_INDEX); - }); - }); - }; - - /** - * @summary Show error dialog for an Error instance - * @function - * @public - * - * @param {Error} error - error - * - * @example - * OSDialogService.showError(new Error('Foo Bar')); - */ - this.showError = (error) => { - const title = errors.getTitle(error); - const message = errors.getDescription(error); - electron.remote.dialog.showErrorBox(title, message); - }; - -}; diff --git a/lib/gui/pages/main/controllers/drive-selection.js b/lib/gui/pages/main/controllers/drive-selection.js index 850398ed..8c88ec99 100644 --- a/lib/gui/pages/main/controllers/drive-selection.js +++ b/lib/gui/pages/main/controllers/drive-selection.js @@ -19,8 +19,9 @@ const settings = require('../../../models/settings'); const selectionState = require('../../../models/selection-state'); const analytics = require('../../../modules/analytics'); +const exceptionReporter = require('../../../modules/exception-reporter'); -module.exports = function(ErrorService, DriveSelectorService) { +module.exports = function(DriveSelectorService) { /** * @summary Open drive selector @@ -42,7 +43,7 @@ module.exports = function(ErrorService, DriveSelectorService) { device: drive.device, unsafeMode: settings.get('unsafeMode') }); - }).catch(ErrorService.reportException); + }).catch(exceptionReporter.report); }; /** diff --git a/lib/gui/pages/main/controllers/flash.js b/lib/gui/pages/main/controllers/flash.js index 56da4eec..bce4b1cf 100644 --- a/lib/gui/pages/main/controllers/flash.js +++ b/lib/gui/pages/main/controllers/flash.js @@ -27,8 +27,7 @@ const path = require('path'); module.exports = function( $state, ImageWriterService, - FlashErrorModalService, - ErrorService + FlashErrorModalService ) { /** @@ -96,7 +95,7 @@ module.exports = function( FlashErrorModalService.show(messages.error.notEnoughSpaceInDrive()); } else { FlashErrorModalService.show(messages.error.genericFlashError()); - ErrorService.reportException(error); + error.reportException(error); } }) diff --git a/lib/gui/pages/main/controllers/image-selection.js b/lib/gui/pages/main/controllers/image-selection.js index 79c76bd8..57dd4b05 100644 --- a/lib/gui/pages/main/controllers/image-selection.js +++ b/lib/gui/pages/main/controllers/image-selection.js @@ -25,10 +25,11 @@ const imageStream = require('../../../../image-stream'); const supportedFormats = require('../../../../shared/supported-formats'); const analytics = require('../../../modules/analytics'); const selectionState = require('../../../models/selection-state'); +const osDialog = require('../../../os/dialog'); +const exceptionReporter = require('../../../modules/exception-reporter'); module.exports = function( - ErrorService, - OSDialogService, + $timeout, WarningModalService ) { @@ -63,7 +64,7 @@ module.exports = function( * @param {Object} image - image * * @example - * OSDialogService.selectImage() + * osDialogService.selectImage() * .then(ImageSelectionController.selectImage); */ this.selectImage = (image) => { @@ -75,7 +76,7 @@ module.exports = function( }) }); - OSDialogService.showError(invalidImageError); + osDialog.showError(invalidImageError); analytics.logEvent('Invalid image', image); return; } @@ -117,7 +118,7 @@ module.exports = function( image.bmap = Boolean(image.bmap); return analytics.logEvent('Select image', image); - }).catch(ErrorService.reportException); + }).catch(exceptionReporter.report); }; /** @@ -132,7 +133,11 @@ module.exports = function( */ this.selectImageByPath = (imagePath) => { imageStream.getImageMetadata(imagePath) - .then(this.selectImage) + .then((imageMetadata) => { + $timeout(() => { + this.selectImage(imageMetadata); + }); + }) .catch((error) => { const imageError = errors.createUserError({ title: 'Error opening image', @@ -142,7 +147,7 @@ module.exports = function( }) }); - OSDialogService.showError(imageError); + osDialog.showError(imageError); analytics.logException(error); }); }; @@ -158,7 +163,7 @@ module.exports = function( this.openImageSelector = () => { analytics.logEvent('Open image selector'); - OSDialogService.selectImage().then((imagePath) => { + osDialog.selectImage().then((imagePath) => { // Avoid analytics and selection state changes // if no file was resolved from the dialog. @@ -168,7 +173,7 @@ module.exports = function( } this.selectImageByPath(imagePath); - }).catch(ErrorService.reportException); + }).catch(exceptionReporter.report); }; /** diff --git a/lib/gui/pages/main/controllers/main.js b/lib/gui/pages/main/controllers/main.js index d01f066c..b76de1ee 100644 --- a/lib/gui/pages/main/controllers/main.js +++ b/lib/gui/pages/main/controllers/main.js @@ -19,12 +19,12 @@ const settings = require('../../../models/settings'); const flashState = require('../../../models/flash-state'); const analytics = require('../../../modules/analytics'); +const exceptionReporter = require('../../../modules/exception-reporter'); const availableDrives = require('../../../models/available-drives'); const selectionState = require('../../../models/selection-state'); module.exports = function( TooltipModalService, - ErrorService, OSOpenExternalService ) { @@ -85,7 +85,7 @@ module.exports = function( return TooltipModalService.show({ title: 'Image File Name', message: selectionState.getImagePath() - }).catch(ErrorService.reportException); + }).catch(exceptionReporter.report); }; }; diff --git a/lib/gui/pages/main/main.js b/lib/gui/pages/main/main.js index cf583c39..62143740 100644 --- a/lib/gui/pages/main/main.js +++ b/lib/gui/pages/main/main.js @@ -39,12 +39,10 @@ const MainPage = angular.module(MODULE_NAME, [ require('../../components/flash-error-modal/flash-error-modal'), require('../../components/progress-button/progress-button'), - require('../../os/dialog/dialog'), require('../../os/open-external/open-external'), require('../../os/dropzone/dropzone'), require('../../modules/image-writer'), - require('../../modules/error'), require('../../utils/byte-size/byte-size') ]); diff --git a/lib/gui/pages/settings/controllers/settings.js b/lib/gui/pages/settings/controllers/settings.js index cd1be236..9e9b7725 100644 --- a/lib/gui/pages/settings/controllers/settings.js +++ b/lib/gui/pages/settings/controllers/settings.js @@ -20,8 +20,9 @@ const os = require('os'); const _ = require('lodash'); const settings = require('../../../models/settings'); const analytics = require('../../../modules/analytics'); +const exceptionReporter = require('../../../modules/exception-reporter'); -module.exports = function(WarningModalService, ErrorService) { +module.exports = function(WarningModalService) { /** * @summary Client platform @@ -100,7 +101,7 @@ module.exports = function(WarningModalService, ErrorService) { this.model.set(setting, true); this.refreshSettings(); } - }).catch(ErrorService.reportException); + }).catch(exceptionReporter.report); }; }; diff --git a/lib/gui/pages/settings/settings.js b/lib/gui/pages/settings/settings.js index 58ff95c2..8bf1c41d 100644 --- a/lib/gui/pages/settings/settings.js +++ b/lib/gui/pages/settings/settings.js @@ -24,8 +24,7 @@ const angular = require('angular'); const MODULE_NAME = 'Etcher.Pages.Settings'; const SettingsPage = angular.module(MODULE_NAME, [ require('angular-ui-router'), - require('../../components/warning-modal/warning-modal'), - require('../../modules/error') + require('../../components/warning-modal/warning-modal') ]); SettingsPage.controller('SettingsController', require('./controllers/settings'));