Merge pull request #172 from resin-io/feat/router

Make use of UI Router
This commit is contained in:
Juan Cruz Viotti 2016-03-02 11:02:09 -04:00
commit 7899e2ed59
12 changed files with 212 additions and 297 deletions

View File

@ -1250,7 +1250,7 @@ mark,
.text-left {
text-align: left; }
.text-right {
.text-right, .section-header {
text-align: right; }
.text-center {
@ -5854,23 +5854,13 @@ button.close {
margin-top: 15px;
margin-bottom: 15px; }
.space-top-huge {
margin-top: 45px; }
.space-vertical-large {
margin-top: 30px;
margin-bottom: 30px; }
.space-horizontal-large {
margin-left: 30px;
margin-right: 30px; }
.space-bottom-large {
margin-bottom: 30px; }
.space-bottom-huge {
margin-bottom: 45px; }
.space-right-tiny {
margin-right: 5px; }
@ -5981,8 +5971,13 @@ body {
.checkbox input[type="checkbox"]:not(:checked) + * {
color: #ddd; }
.btn-navigation {
position: fixed;
top: 10px;
right: 10px;
.wrapper {
height: 100%;
margin: 20px 60px; }
h1, h2, h3, h4, h5, h6 {
margin-top: 0; }
.section-header {
padding: 5px;
font-size: 15px; }

View File

@ -29,6 +29,7 @@ const BrowserWindow = electron.remote.BrowserWindow;
const currentWindow = BrowserWindow.fromId(1);
require('angular-ui-bootstrap');
require('angular-ui-router');
require('./browser/modules/selection-state');
require('./browser/modules/settings');
require('./browser/modules/drive-scanner');
@ -37,6 +38,7 @@ require('./browser/modules/path');
require('./browser/modules/analytics');
const app = angular.module('Etcher', [
'ui.router',
'ui.bootstrap',
// Etcher modules
@ -48,55 +50,71 @@ const app = angular.module('Etcher', [
'Etcher.analytics'
]);
app.config(function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/main');
$stateProvider
.state('main', {
url: '/main',
controller: 'AppController as app',
templateUrl: './partials/main.html'
})
.state('success', {
url: '/success',
controller: 'FinishController as finish',
templateUrl: './partials/success.html'
})
.state('settings', {
url: '/settings',
controller: 'SettingsController as settings',
templateUrl: './partials/settings.html'
});
});
app.controller('AppController', function(
$q,
$state,
DriveScannerService,
SettingsService,
SelectionStateService,
ImageWriterService,
AnalyticsService
) {
let self = this;
this.settings = SettingsService;
this.selection = SelectionStateService;
this.writer = ImageWriterService;
this.scanner = DriveScannerService;
this.restart = function(options) {
AnalyticsService.logEvent('Restart');
this.selection.clear(options);
AnalyticsService.logEvent('Restart');
self.state = {
if (!this.writer.isBurning()) {
this.state = {
progress: 0,
percentage: 0
};
}
this.scanner.start(2000).on('scan', function(drives) {
this.scanner.start(2000).on('scan', function(drives) {
// Notice we only autoselect the drive if there is an image,
// which means that the first step was completed successfully,
// otherwise the drive is selected while the drive step is disabled
// which looks very weird.
if (drives.length === 1 && self.selection.hasImage()) {
const drive = _.first(drives);
// Do not autoselect the same drive over and over again
// and fill the logs unnecessary.
// `angular.equals` is used instead of `_.isEqual` to
// cope with `$$hashKey`.
if (!angular.equals(self.selection.getDrive(), drive)) {
AnalyticsService.logEvent('Auto-select drive', {
device: drive.device
});
self.selectDrive(drive);
}
// Notice we only autoselect the drive if there is an image,
// which means that the first step was completed successfully,
// otherwise the drive is selected while the drive step is disabled
// which looks very weird.
if (drives.length === 1 && self.selection.hasImage()) {
const drive = _.first(drives);
// Do not autoselect the same drive over and over again
// and fill the logs unnecessary.
// `angular.equals` is used instead of `_.isEqual` to
// cope with `$$hashKey`.
if (!angular.equals(self.selection.getDrive(), drive)) {
AnalyticsService.logEvent('Auto-select drive', {
device: drive.device
});
self.selectDrive(drive);
}
});
};
this.restart();
}
});
// We manually add `style="display: none;"` to <body>
// and unset it here instead of using ngCloak since
@ -167,6 +185,7 @@ app.controller('AppController', function(
}).then(function() {
AnalyticsService.logEvent('Done');
$state.go('success');
}).catch(dialog.showError).finally(function() {
// Remove progress bar from task bar
@ -179,3 +198,18 @@ app.controller('AppController', function(
this.open = shell.openExternal;
});
app.controller('SettingsController', function(SettingsService) {
this.storage = SettingsService.data;
});
app.controller('NavigationController', function($state) {
this.isState = $state.is;
});
app.controller('FinishController', function($state, SelectionStateService) {
this.restart = function(options) {
SelectionStateService.clear(options);
$state.go('main');
};
});

View File

@ -38,57 +38,4 @@ settings.service('SettingsService', function($localStorage) {
errorReporting: true
});
// All this functionality should be gone once
// we make use of a real router on the application.
// This is not the case yet since when the application
// was first prototyped there was only one screen
// and therefore a router would have been overkill.
/**
* @summary Configuring state
* @type Boolean
* @private
*/
let configuring = false;
/**
* @summary Check if the user is configuring
* @function
* @public
*
* @returns {Boolean} whether is configuring
*
* @example
* if (SettingsService.isConfiguring()) {
* console.log('User is on settings screen');
* }
*/
this.isConfiguring = function() {
return configuring;
};
/**
* @summary Enter settings screen
* @function
* @public
*
* @example
* SettingsService.enter();
*/
this.enter = function() {
configuring = true;
};
/**
* @summary Leave settings screen
* @function
* @public
*
* @example
* SettingsService.leave();
*/
this.leave = function() {
configuring = false;
};
});

View File

@ -35,6 +35,8 @@ body {
-webkit-overflow-scrolling: touch;
}
/* Prevent blue outline */
input:focus,
button:focus {
outline: none !important;
}

View File

@ -24,142 +24,20 @@
<script src="./browser/app.js"></script>
</head>
<body ng-app="Etcher" ng-controller="AppController as app" style="display: none">
<div class="content row middle-xs space-horizontal-large" ng-hide="app.settings.isConfiguring()">
<button class="btn btn-link btn-navigation" ng-click="app.settings.enter()">
<body ng-app="Etcher" style="display: none">
<header class="section-header" ng-controller="NavigationController as navigation">
<button class="btn btn-link" ui-sref="settings" ng-hide="navigation.isState('settings')">
<span class="glyphicon glyphicon-cog"></span>
</button>
<div class="col-xs">
<div class="row around-xs space-bottom-huge" ng-hide="app.state.progress == 100 && !app.writer.isBurning()">
<div class="col-xs">
<div class="box text-center">
<hero-icon path="images/image.svg" label="SELECT IMAGE"></hero-icon>
<hero-badge class="block space-vertical-medium">1</hero-badge>
<button class="btn btn-link" ui-sref="main" ng-show="navigation.isState('settings')">
<span class="glyphicon glyphicon-chevron-left"></span> Back
</button>
</header>
<div class="space-vertical-large">
<div ng-hide="app.selection.hasImage()">
<hero-button ng-click="app.selectImage()">Select image</hero-button>
<p class="step-footer tiny">*supported files: .img, .iso</p>
</div>
<div ng-show="app.selection.hasImage()">
<span ng-bind="app.selection.getImage() | basename" ng-click="app.reselectImage()"></span>
</div>
</div>
</div>
</div>
<main class="wrapper" ui-view></main>
<div class="col-xs">
<div class="box text-center relative">
<div class="step-border-left" ng-disabled="!app.selection.hasImage()"></div>
<div class="step-border-right" ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()"></div>
<hero-icon path="images/drive.svg" ng-disabled="!app.selection.hasImage()" label="SELECT DRIVE"></hero-icon>
<hero-badge class="block space-vertical-medium" ng-disabled="!app.selection.hasImage()">2</hero-badge>
<div class="space-vertical-large">
<div ng-hide="app.selection.hasDrive()">
<div ng-show="app.scanner.hasAvailableDrives() || !app.selection.hasImage()">
<div class="btn-group" uib-dropdown>
<hero-button ng-disabled="!app.selection.hasImage()"
uib-dropdown-toggle>Select drive</hero-button>
<ul class="dropdown-menu">
<li ng-repeat="drive in app.scanner.drives">
<a href="#" ng-click="app.selectDrive(drive)" ng-bind="drive.name + ' - ' + drive.size"></a>
</li>
</ul>
</div>
</div>
<div ng-hide="app.scanner.hasAvailableDrives() || !app.selection.hasImage()">
<hero-button type="danger">Connect a drive</hero-button>
</div>
</div>
<div ng-show="app.selection.hasDrive()" ng-bind="app.selection.getDrive().name" ng-click="app.reselectDrive()"></div>
</div>
</div>
</div>
<div class="col-xs">
<div class="box text-center">
<hero-icon path="images/burn.svg" ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()" label="BURN IMAGE"></hero-icon>
<hero-badge class="block space-vertical-medium" ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()">3</hero-badge>
<div class="space-vertical-large">
<hero-progress-button percentage="{{ app.state.progress }}" ng-attr-active="{{ app.writer.isBurning() }}"
ng-click="app.burn(app.selection.getImage(), app.selection.getDrive())"
ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()">
<span ng-show="app.state.progress == 100 && app.writer.isBurning()">Finishing...</span>
<span ng-show="app.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.state.speed && app.state.progress != 100"
ng-bind="app.state.progress + '% '"></span>
</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>
</div>
</div>
</div>
</div>
<div class="row around-xs space-bottom-huge" ng-show="app.state.progress == 100 && !app.writer.isBurning()">
<div class="col-xs">
<div class="box text-center">
<h3><hero-tick type="success" class="space-right-tiny"></hero-tick> Burn Complete!</h3>
<p class="soft">Safely ejected and ready for use</p>
<div class="row center-xs space-vertical-large">
<div class="col-xs-4 space-medium">
<div class="box">
<p class="soft button-label">Would you like to burn the same image?</p>
<hero-button ng-click="app.restart({ preserveImage: true })">
Use <b>same</b> image
</hero-button>
</div>
</div>
<div class="col-xs separator-xs"></div>
<div class="col-xs-4 space-medium">
<div class="box">
<p class="soft button-label">Would you like to burn a new image?</p>
<hero-button ng-click="app.restart()">
Use <b>new</b> image
</hero-button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="content row space-horizontal-large space-top-huge" ng-show="app.settings.isConfiguring()">
<div class="col-xs" ng-show="app.settings.isConfiguring()">
<button class="btn btn-link btn-navigation" ng-click="app.settings.leave()">
<span class="glyphicon glyphicon-chevron-left"></span> Back
</button>
<div class="box text-left space-horizontal-large">
<h1 class="space-bottom-large">Settings</h1>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="app.settings.data.errorReporting">
<span>Enable error reporting</span>
</label>
</div>
</div>
</div>
</div>
<div class="section-footer row between-xs middle-xs">
<footer class="section-footer row between-xs middle-xs">
<div class="col-xs">
<div class="box text-left">
<hero-icon path="images/resin.svg" width="85px" height="auto" ng-click="app.open('https://resin.io')"></hero-icon>
@ -171,6 +49,6 @@
<hero-caption><span ng-click="app.open('https://github.com/resin-io/etcher')">AN OPEN SOURCE PROJECT</span> BY <span ng-click="app.open('https://resin.io')">RESIN.IO</span></hero-caption>
</div>
</div>
</div>
</footer>
</body>
</html>

73
lib/partials/main.html Normal file
View File

@ -0,0 +1,73 @@
<div class="row around-xs">
<div class="col-xs">
<div class="box text-center">
<hero-icon path="images/image.svg" label="SELECT IMAGE"></hero-icon>
<hero-badge class="block space-vertical-medium">1</hero-badge>
<div class="space-vertical-large">
<div ng-hide="app.selection.hasImage()">
<hero-button ng-click="app.selectImage()">Select image</hero-button>
<p class="step-footer tiny">*supported files: .img, .iso</p>
</div>
<div ng-show="app.selection.hasImage()">
<span ng-bind="app.selection.getImage() | basename" ng-click="app.reselectImage()"></span>
</div>
</div>
</div>
</div>
<div class="col-xs">
<div class="box text-center relative">
<div class="step-border-left" ng-disabled="!app.selection.hasImage()"></div>
<div class="step-border-right" ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()"></div>
<hero-icon path="images/drive.svg" ng-disabled="!app.selection.hasImage()" label="SELECT DRIVE"></hero-icon>
<hero-badge class="block space-vertical-medium" ng-disabled="!app.selection.hasImage()">2</hero-badge>
<div class="space-vertical-large">
<div ng-hide="app.selection.hasDrive()">
<div ng-show="app.scanner.hasAvailableDrives() || !app.selection.hasImage()">
<div class="btn-group" uib-dropdown>
<hero-button ng-disabled="!app.selection.hasImage()"
uib-dropdown-toggle>Select drive</hero-button>
<ul class="dropdown-menu">
<li ng-repeat="drive in app.scanner.drives">
<a href="#" ng-click="app.selectDrive(drive)" ng-bind="drive.name + ' - ' + drive.size"></a>
</li>
</ul>
</div>
</div>
<div ng-hide="app.scanner.hasAvailableDrives() || !app.selection.hasImage()">
<hero-button type="danger">Connect a drive</hero-button>
</div>
</div>
<div ng-show="app.selection.hasDrive()" ng-bind="app.selection.getDrive().name" ng-click="app.reselectDrive()"></div>
</div>
</div>
</div>
<div class="col-xs">
<div class="box text-center">
<hero-icon path="images/burn.svg" ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()" label="BURN IMAGE"></hero-icon>
<hero-badge class="block space-vertical-medium" ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()">3</hero-badge>
<div class="space-vertical-large">
<hero-progress-button percentage="{{ app.state.progress }}" ng-attr-active="{{ app.writer.isBurning() }}"
ng-click="app.burn(app.selection.getImage(), app.selection.getDrive())"
ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()">
<span ng-show="app.state.progress == 100 && app.writer.isBurning()">Finishing...</span>
<span ng-show="app.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.state.speed && app.state.progress != 100"
ng-bind="app.state.progress + '% '"></span>
</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>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,10 @@
<div class="text-left">
<h1 class="space-bottom-large">Settings</h1>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="settings.storage.errorReporting">
<span>Enable error reporting</span>
</label>
</div>
</div>

32
lib/partials/success.html Normal file
View File

@ -0,0 +1,32 @@
<div class="row around-xs">
<div class="col-xs">
<div class="box text-center">
<h3><hero-tick type="success" class="space-right-tiny"></hero-tick> Burn Complete!</h3>
<p class="soft">Safely ejected and ready for use</p>
<div class="row center-xs space-vertical-large">
<div class="col-xs-4 space-medium">
<div class="box">
<p class="soft button-label">Would you like to burn the same image?</p>
<hero-button ng-click="finish.restart({ preserveImage: true })">
Use <b>same</b> image
</hero-button>
</div>
</div>
<div class="col-xs separator-xs"></div>
<div class="col-xs-4 space-medium">
<div class="box">
<p class="soft button-label">Would you like to burn a new image?</p>
<hero-button ng-click="finish.restart()">
Use <b>new</b> image
</hero-button>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -184,9 +184,17 @@ body {
color: $gray-light;
}
.btn-navigation {
position: fixed;
top: 10px;
right: 10px;
.wrapper {
height: 100%;
margin: 20px 60px;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
}
.section-header {
@extend .text-right;
padding: 5px;
font-size: 15px;
}

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
$spacing-huge: 45px;
$spacing-large: 30px;
$spacing-medium: 15px;
$spacing-tiny: 5px;
@ -28,28 +27,15 @@ $spacing-tiny: 5px;
margin-bottom: $spacing-medium;
}
.space-top-huge {
margin-top: $spacing-huge;
}
.space-vertical-large {
margin-top: $spacing-large;
margin-bottom: $spacing-large;
}
.space-horizontal-large {
margin-left: $spacing-large;
margin-right: $spacing-large;
}
.space-bottom-large {
margin-bottom: $spacing-large;
}
.space-bottom-huge {
margin-bottom: $spacing-huge;
}
.space-right-tiny {
margin-right: $spacing-tiny;
}

View File

@ -52,6 +52,7 @@
"dependencies": {
"angular": "^1.4.6",
"angular-ui-bootstrap": "^1.2.1",
"angular-ui-router": "^0.2.18",
"bluebird": "^3.0.5",
"bootstrap-sass": "^3.3.5",
"drivelist": "^2.0.7",

View File

@ -1,51 +0,0 @@
'use strict';
const m = require('mochainon');
const angular = require('angular');
require('angular-mocks');
require('../../../lib/browser/modules/settings');
describe('Browser: Settings', function() {
beforeEach(angular.mock.module('Etcher.settings'));
describe('SettingsService', function() {
let SettingsService;
beforeEach(angular.mock.inject(function(_SettingsService_) {
SettingsService = _SettingsService_;
}));
describe('.isConfiguring()', function() {
it('should initially return false', function() {
m.chai.expect(SettingsService.isConfiguring()).to.be.false;
});
});
describe('.enter()', function() {
it('should be able to enter settings', function() {
m.chai.expect(SettingsService.isConfiguring()).to.be.false;
SettingsService.enter();
m.chai.expect(SettingsService.isConfiguring()).to.be.true;
});
});
describe('.leave()', function() {
it('should be able to leave settings', function() {
SettingsService.enter();
m.chai.expect(SettingsService.isConfiguring()).to.be.true;
SettingsService.leave();
m.chai.expect(SettingsService.isConfiguring()).to.be.false;
});
});
});
});