diff --git a/lib/gui/app.js b/lib/gui/app.js index 3a279f06..2b93f8d2 100644 --- a/lib/gui/app.js +++ b/lib/gui/app.js @@ -22,8 +22,6 @@ var angular = require('angular'); const _ = require('lodash'); -const electron = require('electron'); -const dialog = electron.remote.require('./gui/dialog'); const app = angular.module('Etcher', [ require('angular-ui-router'), @@ -51,6 +49,7 @@ const app = angular.module('Etcher', [ require('./os/window-progress/window-progress'), require('./os/open-external/open-external'), require('./os/dropzone/dropzone'), + require('./os/dialog/dialog'), // Utils require('./utils/if-state/if-state'), @@ -76,7 +75,6 @@ app.config(function($stateProvider, $urlRouterProvider) { }); app.controller('AppController', function( - $q, $state, $scope, NotifierService, @@ -87,7 +85,8 @@ app.controller('AppController', function( AnalyticsService, DriveSelectorService, OSWindowProgressService, - OSNotificationService + OSNotificationService, + OSDialogService ) { let self = this; this.selection = SelectionStateModel; @@ -117,7 +116,7 @@ app.controller('AppController', function( OSWindowProgressService.set(state.progress); }); - this.scanner.start(2000).on('error', dialog.showError).on('scan', function(drives) { + this.scanner.start(2000).on('error', OSDialogService.showError).on('scan', function(drives) { // Cover the case where you select a drive, but then eject it. if (self.selection.hasDrive() && !_.find(drives, self.selection.isCurrentDrive)) { @@ -167,7 +166,7 @@ app.controller('AppController', function( }; this.openImageSelector = function() { - return $q.when(dialog.selectImage()).then(function(image) { + return OSDialogService.selectImage().then(function(image) { // Avoid analytics and selection state changes // if no file was resolved from the dialog. @@ -260,7 +259,7 @@ app.controller('AppController', function( } self.writer.resetState(); - dialog.showError(error); + OSDialogService.showError(error); }) .finally(OSWindowProgressService.clear); }; diff --git a/lib/gui/dialog.js b/lib/gui/dialog.js deleted file mode 100644 index ff6923cf..00000000 --- a/lib/gui/dialog.js +++ /dev/null @@ -1,88 +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 electron = require('electron'); -const Bluebird = require('bluebird'); -const zipImage = require('resin-zip-image'); -const packageJSON = require('../../package.json'); - -/** - * @summary Open an image selection dialog - * @function - * @public - * - * @description - * Notice that by image, we mean *.img/*.iso/*.zip files. - * - * If the user selects an invalid zip image, an error alert - * is shown, and the promise resolves `undefined`. - * - * @fulfil {String} - selected image - * @returns {Promise}; - * - * @example - * dialog.selectImage().then(function(image) { - * console.log('The selected image is', image); - * }); - */ -exports.selectImage = function() { - return new Bluebird(function(resolve) { - electron.dialog.showOpenDialog({ - properties: [ 'openFile' ], - filters: [ - { - name: 'IMG/ISO/ZIP', - extensions: [ - 'zip', - 'img', - 'iso' - ] - } - ] - }, function(files) { - return resolve(files || []); - }); - }).get(0).then(function(file) { - if (file && zipImage.isZip(file) && !zipImage.isValidZipImage(file)) { - electron.dialog.showErrorBox( - 'Invalid zip image', - `${packageJSON.displayName} can only open Zip archives that contain exactly one image file inside.` - ); - - return; - } - - return file; - }); -}; - -/** - * @summary Show error dialog for an Error instance - * @function - * @public - * - * @param {Error} error - error - * - * @example - * dialog.showError(new Error('Foo Bar')); - */ -exports.showError = function(error) { - error = error || {}; - electron.dialog.showErrorBox(error.message || 'An error ocurred!', error.stack || ''); - throw error; -}; diff --git a/lib/gui/os/dialog/dialog.js b/lib/gui/os/dialog/dialog.js new file mode 100644 index 00000000..ce2c8572 --- /dev/null +++ b/lib/gui/os/dialog/dialog.js @@ -0,0 +1,31 @@ +/* + * 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.OS.Dialog + * + * The purpose of this module is to provide an easy way + * to interact with OS dialogs. + */ + +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; diff --git a/lib/gui/os/dialog/services/dialog.js b/lib/gui/os/dialog/services/dialog.js new file mode 100644 index 00000000..2b808976 --- /dev/null +++ b/lib/gui/os/dialog/services/dialog.js @@ -0,0 +1,84 @@ +/* + * 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 imageStream = require('etcher-image-stream'); + +module.exports = function($q) { + + /** + * @summary Open an image selection dialog + * @function + * @public + * + * @description + * Notice that by image, we mean *.img/*.iso/*.zip/etc files. + * + * @fulfil {String} - selected image + * @returns {Promise}; + * + * @example + * OSDialogService.selectImage().then(function(image) { + * console.log('The selected image is', image); + * }); + */ + this.selectImage = function() { + return $q(function(resolve) { + electron.remote.dialog.showOpenDialog({ + properties: [ + 'openFile' + ], + filters: [ + { + name: 'OS Images', + extensions: imageStream.supportedFileTypes + } + ] + }, function(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 Show error dialog for an Error instance + * @function + * @public + * + * @param {Error} error - error + * + * @example + * OSDialogService.showError(new Error('Foo Bar')); + */ + this.showError = function(error) { + error = error || {}; + electron.remote.dialog.showErrorBox(error.message || 'An error ocurred!', error.stack || ''); + + // Also throw it so it gets displayed in DevTools + // and its reported by TrackJS. + throw error; + + }; + +}; diff --git a/tests/src/dialog.spec.js b/tests/src/dialog.spec.js deleted file mode 100644 index adb847e3..00000000 --- a/tests/src/dialog.spec.js +++ /dev/null @@ -1,49 +0,0 @@ -'use strict'; - -const m = require('mochainon'); -const electron = require('electron'); -const dialog = require('../../lib/gui/dialog'); - -describe('Dialog:', function() { - - describe('.selectImage()', function() { - - describe('given the user does not select anything', function() { - - beforeEach(function() { - this.showOpenDialogStub = m.sinon.stub(electron.dialog, 'showOpenDialog'); - this.showOpenDialogStub.yields(undefined); - }); - - afterEach(function() { - this.showOpenDialogStub.restore(); - }); - - it('should eventually be undefined', function() { - const promise = dialog.selectImage(); - m.chai.expect(promise).to.eventually.be.undefined; - }); - - }); - - describe('given the users performs a selection', function() { - - beforeEach(function() { - this.showOpenDialogStub = m.sinon.stub(electron.dialog, 'showOpenDialog'); - this.showOpenDialogStub.yields([ 'foo/bar' ]); - }); - - afterEach(function() { - this.showOpenDialogStub.restore(); - }); - - it('should eventually equal the file', function() { - const promise = dialog.selectImage(); - m.chai.expect(promise).to.eventually.equal('foo/bar'); - }); - - }); - - }); - -});