From eef1d51a7a080c0652e1b95b65be4b87236ca356 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Wed, 27 Jul 2016 22:22:52 -0400 Subject: [PATCH] refactor(GUI): extract MainPage from `lib/gui/app.js` (#607) `lib/gui/app.js` contains a lot of code that should have been split long ago. This PR extracts the "main page" logic into an actual page component in `lib/gui/pages`. Signed-off-by: Juan Cruz Viotti --- docs/ARCHITECTURE.md | 6 +- lib/gui/app.js | 235 +-------------------- lib/gui/pages/main/controllers/main.js | 225 ++++++++++++++++++++ lib/gui/pages/main/main.js | 67 ++++++ lib/gui/pages/main/templates/main.tpl.html | 126 +++++++++++ lib/gui/partials/main.html | 126 ----------- 6 files changed, 424 insertions(+), 361 deletions(-) create mode 100644 lib/gui/pages/main/controllers/main.js create mode 100644 lib/gui/pages/main/main.js create mode 100644 lib/gui/pages/main/templates/main.tpl.html delete mode 100644 lib/gui/partials/main.html diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index cfb2ad68..8a1b680a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -91,7 +91,7 @@ straightforward interfaces as AngularJS modules, and provide a single place where all the modules are tied together. Therefore, if you want to get a rough idea of how the GUI works, the perfect -place to start is [application controller][appcontroller] and the [main +place to start is [main controller][maincontroller] and the [main view][mainview], and diving into specific modules depending on your interests. Summary @@ -106,8 +106,8 @@ be documented instead! [lego-blocks]: https://github.com/sindresorhus/ama/issues/10#issuecomment-117766328 [etcher-image-write]: https://github.com/resin-io-modules/etcher-image-write [exit-codes]: https://github.com/resin-io/etcher/blob/master/lib/src/exit-codes.js -[appcontroller]: https://github.com/resin-io/etcher/blob/master/lib/gui/app.js -[mainview]: https://github.com/resin-io/etcher/blob/master/lib/gui/partials/main.html +[maincontroller]: https://github.com/resin-io/etcher/blob/master/lib/gui/pages/main/controllers/main.js +[mainview]: https://github.com/resin-io/etcher/blob/master/lib/gui/pages/main/templates/main.tpl.html [cli-dir]: https://github.com/resin-io/etcher/tree/master/lib/cli [gui-dir]: https://github.com/resin-io/etcher/tree/master/lib/gui diff --git a/lib/gui/app.js b/lib/gui/app.js index 7742e8bc..388fc7a5 100644 --- a/lib/gui/app.js +++ b/lib/gui/app.js @@ -26,51 +26,35 @@ var angular = require('angular'); /* eslint-enable no-var */ -const _ = require('lodash'); const Store = require('./models/store'); const app = angular.module('Etcher', [ require('angular-ui-router'), require('angular-ui-bootstrap'), - require('angular-moment'), - require('angular-middle-ellipses'), require('angular-if-state'), - require('angular-seconds-to-date'), // Etcher modules - require('./modules/drive-scanner'), - require('./modules/image-writer'), require('./modules/analytics'), // Models require('./models/selection-state'), - require('./models/settings'), - require('./models/supported-formats'), - require('./models/drives'), require('./models/flash-state'), // Components - require('./components/progress-button/progress-button'), - require('./components/drive-selector/drive-selector'), require('./components/svg-icon/svg-icon'), require('./components/update-notifier/update-notifier'), - require('./components/tooltip-modal/tooltip-modal'), // Pages + require('./pages/main/main'), require('./pages/finish/finish'), require('./pages/settings/settings'), // OS - require('./os/notification/notification'), require('./os/window-progress/window-progress'), require('./os/open-external/open-external'), - require('./os/dropzone/dropzone'), - require('./os/dialog/dialog'), // Utils - require('./utils/path/path'), - require('./utils/manifest-bind/manifest-bind'), - require('./utils/byte-size/byte-size') + require('./utils/manifest-bind/manifest-bind') ]); app.run((AnalyticsService, UpdateNotifierService, SelectionStateModel) => { @@ -121,219 +105,6 @@ app.run((AnalyticsService, OSWindowProgressService, FlashStateModel) => { }); }); -app.config(($stateProvider, $urlRouterProvider) => { +app.config(($urlRouterProvider) => { $urlRouterProvider.otherwise('/main'); - - $stateProvider - .state('main', { - url: '/main', - controller: 'AppController as app', - templateUrl: './partials/main.html' - }); -}); - -app.controller('AppController', function( - $state, - DriveScannerService, - SelectionStateModel, - FlashStateModel, - SettingsModel, - SupportedFormatsModel, - DrivesModel, - ImageWriterService, - AnalyticsService, - DriveSelectorService, - UpdateNotifierService, - TooltipModalService, - OSWindowProgressService, - OSNotificationService, - OSDialogService, - OSOpenExternalService -) { - this.formats = SupportedFormatsModel; - this.selection = SelectionStateModel; - this.drives = DrivesModel; - this.writer = ImageWriterService; - this.state = FlashStateModel; - this.settings = SettingsModel; - this.tooltipModal = TooltipModalService; - - this.handleError = (error) => { - - // This particular error is handled by the alert ribbon - // on the main application page. - if (error.code === 'ENOSPC') { - AnalyticsService.logEvent('Drive ran out of space'); - return; - } - - OSDialogService.showError(error); - - // Also throw it so it gets displayed in DevTools - // and its reported by TrackJS. - throw error; - }; - - // This catches the case where the user enters - // the settings screen when a flash finished - // and goes back to the main screen with the back button. - if (!FlashStateModel.isFlashing()) { - - this.selection.clear({ - - // Preserve image, in case there is one, otherwise - // we revert the behaviour of "Use same image". - preserveImage: true - - }); - } - - DriveScannerService.start(); - - DriveScannerService.on('error', this.handleError); - - DriveScannerService.on('drives', (drives) => { - this.drives.setDrives(drives); - - if (_.isEmpty(drives)) { - DriveSelectorService.close(); - } - }); - - this.selectImage = (image) => { - if (!SupportedFormatsModel.isSupportedImage(image.path)) { - OSDialogService.showError('Invalid image', `${image.path} is not a supported image type.`); - AnalyticsService.logEvent('Invalid image', image); - return; - } - - this.selection.setImage(image); - AnalyticsService.logEvent('Select image', _.omit(image, 'logo')); - }; - - this.openImageUrl = () => { - const imageUrl = this.selection.getImageUrl(); - - if (imageUrl) { - OSOpenExternalService.open(imageUrl); - } - }; - - this.openImageSelector = () => { - return OSDialogService.selectImage().then((image) => { - - // Avoid analytics and selection state changes - // if no file was resolved from the dialog. - if (!image) { - return; - } - - this.selectImage(image); - }).catch(this.handleError); - }; - - this.selectDrive = (drive) => { - if (!drive) { - return; - } - - this.selection.setDrive(drive.device); - - AnalyticsService.logEvent('Select drive', { - device: drive.device - }); - }; - - this.openDriveSelector = () => { - DriveSelectorService.open() - .then(this.selectDrive) - .catch(this.handleError); - }; - - this.reselectImage = () => { - if (FlashStateModel.isFlashing()) { - return; - } - - // Reselecting an image automatically - // de-selects the current drive, if any. - // This is made so the user effectively - // "returns" to the first step. - this.selection.clear(); - - this.openImageSelector(); - AnalyticsService.logEvent('Reselect image'); - }; - - this.reselectDrive = () => { - if (FlashStateModel.isFlashing()) { - return; - } - - this.openDriveSelector(); - AnalyticsService.logEvent('Reselect drive'); - }; - - this.restartAfterFailure = () => { - this.selection.clear({ - preserveImage: true - }); - - FlashStateModel.resetState(); - AnalyticsService.logEvent('Restart after failure'); - }; - - this.wasLastFlashSuccessful = () => { - const flashResults = FlashStateModel.getFlashResults(); - - if (_.get(flashResults, 'cancelled', false)) { - return true; - } - - return _.get(flashResults, 'passedValidation', true); - }; - - this.flash = (image, drive) => { - - if (FlashStateModel.isFlashing()) { - return; - } - - // Stop scanning drives when flashing - // otherwise Windows throws EPERM - DriveScannerService.stop(); - - AnalyticsService.logEvent('Flash', { - image: image, - device: drive.device - }); - - return this.writer.flash(image, drive).then(() => { - const results = FlashStateModel.getFlashResults(); - - if (results.cancelled) { - return; - } - - if (results.passedValidation) { - OSNotificationService.send('Success!', 'Your flash is complete'); - AnalyticsService.logEvent('Done'); - $state.go('success'); - } else { - OSNotificationService.send('Oops!', 'Looks like your flash has failed'); - AnalyticsService.logEvent('Validation error'); - } - }) - .catch((error) => { - - if (error.type === 'check') { - AnalyticsService.logEvent('Validation error'); - } else { - AnalyticsService.logEvent('Flash error'); - } - - this.handleError(error); - }) - .finally(OSWindowProgressService.clear); - }; }); diff --git a/lib/gui/pages/main/controllers/main.js b/lib/gui/pages/main/controllers/main.js new file mode 100644 index 00000000..1d5cabe9 --- /dev/null +++ b/lib/gui/pages/main/controllers/main.js @@ -0,0 +1,225 @@ +/* + * 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'); + +module.exports = function( + $state, + DriveScannerService, + SelectionStateModel, + FlashStateModel, + SettingsModel, + SupportedFormatsModel, + DrivesModel, + ImageWriterService, + AnalyticsService, + DriveSelectorService, + TooltipModalService, + OSWindowProgressService, + OSNotificationService, + OSDialogService, + OSOpenExternalService +) { + + this.formats = SupportedFormatsModel; + this.selection = SelectionStateModel; + this.drives = DrivesModel; + this.state = FlashStateModel; + this.settings = SettingsModel; + this.tooltipModal = TooltipModalService; + + const handleError = (error) => { + + // This particular error is handled by the alert ribbon + // on the main application page. + if (error.code === 'ENOSPC') { + AnalyticsService.logEvent('Drive ran out of space'); + return; + } + + OSDialogService.showError(error); + + // Also throw it so it gets displayed in DevTools + // and its reported by TrackJS. + throw error; + }; + + // This catches the case where the user enters + // the settings screen when a flash finished + // and goes back to the main screen with the back button. + if (!FlashStateModel.isFlashing()) { + + this.selection.clear({ + + // Preserve image, in case there is one, otherwise + // we revert the behaviour of "Use same image". + preserveImage: true + + }); + } + + DriveScannerService.start(); + + DriveScannerService.on('error', handleError); + + DriveScannerService.on('drives', (drives) => { + this.drives.setDrives(drives); + + if (_.isEmpty(drives)) { + DriveSelectorService.close(); + } + }); + + this.selectImage = (image) => { + if (!SupportedFormatsModel.isSupportedImage(image.path)) { + OSDialogService.showError('Invalid image', `${image.path} is not a supported image type.`); + AnalyticsService.logEvent('Invalid image', image); + return; + } + + this.selection.setImage(image); + AnalyticsService.logEvent('Select image', _.omit(image, 'logo')); + }; + + this.openImageUrl = () => { + const imageUrl = this.selection.getImageUrl(); + + if (imageUrl) { + OSOpenExternalService.open(imageUrl); + } + }; + + this.openImageSelector = () => { + return OSDialogService.selectImage().then((image) => { + + // Avoid analytics and selection state changes + // if no file was resolved from the dialog. + if (!image) { + return; + } + + this.selectImage(image); + }).catch(handleError); + }; + + this.selectDrive = (drive) => { + if (!drive) { + return; + } + + this.selection.setDrive(drive.device); + + AnalyticsService.logEvent('Select drive', { + device: drive.device + }); + }; + + this.openDriveSelector = () => { + DriveSelectorService.open() + .then(this.selectDrive) + .catch(handleError); + }; + + this.reselectImage = () => { + if (FlashStateModel.isFlashing()) { + return; + } + + // Reselecting an image automatically + // de-selects the current drive, if any. + // This is made so the user effectively + // "returns" to the first step. + this.selection.clear(); + + this.openImageSelector(); + AnalyticsService.logEvent('Reselect image'); + }; + + this.reselectDrive = () => { + if (FlashStateModel.isFlashing()) { + return; + } + + this.openDriveSelector(); + AnalyticsService.logEvent('Reselect drive'); + }; + + this.restartAfterFailure = () => { + this.selection.clear({ + preserveImage: true + }); + + FlashStateModel.resetState(); + AnalyticsService.logEvent('Restart after failure'); + }; + + this.wasLastFlashSuccessful = () => { + const flashResults = FlashStateModel.getFlashResults(); + + if (_.get(flashResults, 'cancelled', false)) { + return true; + } + + return _.get(flashResults, 'passedValidation', true); + }; + + this.flash = (image, drive) => { + + if (FlashStateModel.isFlashing()) { + return; + } + + // Stop scanning drives when flashing + // otherwise Windows throws EPERM + DriveScannerService.stop(); + + AnalyticsService.logEvent('Flash', { + image: image, + device: drive.device + }); + + return ImageWriterService.flash(image, drive).then(() => { + const results = FlashStateModel.getFlashResults(); + + if (results.cancelled) { + return; + } + + if (results.passedValidation) { + OSNotificationService.send('Success!', 'Your flash is complete'); + AnalyticsService.logEvent('Done'); + $state.go('success'); + } else { + OSNotificationService.send('Oops!', 'Looks like your flash has failed'); + AnalyticsService.logEvent('Validation error'); + } + }) + .catch((error) => { + + if (error.type === 'check') { + AnalyticsService.logEvent('Validation error'); + } else { + AnalyticsService.logEvent('Flash error'); + } + + handleError(error); + }) + .finally(OSWindowProgressService.clear); + }; + +}; diff --git a/lib/gui/pages/main/main.js b/lib/gui/pages/main/main.js new file mode 100644 index 00000000..c270a1d7 --- /dev/null +++ b/lib/gui/pages/main/main.js @@ -0,0 +1,67 @@ +/* + * 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.Pages.Main + * + * The finish page represents the application main page. + */ + +const angular = require('angular'); +const MODULE_NAME = 'Etcher.Pages.Main'; + +const MainPage = angular.module(MODULE_NAME, [ + require('angular-ui-router'), + require('angular-moment'), + require('angular-middle-ellipses'), + require('angular-seconds-to-date'), + + require('../../components/drive-selector/drive-selector'), + require('../../components/tooltip-modal/tooltip-modal'), + require('../../components/progress-button/progress-button'), + + require('../../os/window-progress/window-progress'), + require('../../os/notification/notification'), + require('../../os/dialog/dialog'), + require('../../os/open-external/open-external'), + + require('../../modules/drive-scanner'), + require('../../modules/image-writer'), + require('../../modules/analytics'), + require('../../models/selection-state'), + require('../../models/flash-state'), + require('../../models/settings'), + require('../../models/supported-formats'), + require('../../models/drives'), + + require('../../utils/path/path'), + require('../../utils/byte-size/byte-size') +]); + +MainPage.controller('MainController', require('./controllers/main')); + +MainPage.config(($stateProvider) => { + $stateProvider + .state('main', { + url: '/main', + controller: 'MainController as main', + templateUrl: './pages/main/templates/main.tpl.html' + }); +}); + +module.exports = MODULE_NAME; diff --git a/lib/gui/pages/main/templates/main.tpl.html b/lib/gui/pages/main/templates/main.tpl.html new file mode 100644 index 00000000..4a0feaba --- /dev/null +++ b/lib/gui/pages/main/templates/main.tpl.html @@ -0,0 +1,126 @@ +
+
+
+ + SELECT IMAGE + 1 + +
+
+ + + +
+
+
+ + + + +
+
+
+
+ +
+
+
+
+ + + SELECT DRIVE + + 2 + +
+
+ +
+ +
+ +
+ +
+ +
+
+
{{ main.selection.getDrive().name }} - {{ main.selection.getDrive().size | gigabyte | number:1 }} GB
+ +
+
+
+
+ +
+
+ + FLASH IMAGE + + 3 + +
+ + Finishing... + Flash! + Starting... + + + + +
+ + + Not enough space on the drive.
Please insert larger one and +
+ + Your removable drive may be corrupted.
Try inserting a different one and +
+ + Oops, seems something went wrong. Click to retry + +
+ + + + +
+
+
+
diff --git a/lib/gui/partials/main.html b/lib/gui/partials/main.html deleted file mode 100644 index 6bb68088..00000000 --- a/lib/gui/partials/main.html +++ /dev/null @@ -1,126 +0,0 @@ -
-
-
- - SELECT IMAGE - 1 - -
-
- - - -
-
-
- - - - -
-
-
-
- -
-
-
-
- - - SELECT DRIVE - - 2 - -
-
- -
- -
- -
- -
- -
-
-
{{ app.selection.getDrive().name }} - {{ app.selection.getDrive().size | gigabyte | number:1 }} GB
- -
-
-
-
- -
-
- - FLASH IMAGE - - 3 - -
- - Finishing... - Flash! - Starting... - - - - -
- - - Not enough space on the drive.
Please insert larger one and -
- - Your removable drive may be corrupted.
Try inserting a different one and -
- - Oops, seems something went wrong. Click to retry - -
- - - - -
-
-
-