mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-19 17:26:34 +00:00
Implement write validation support
* Extend ProgressButton to support a striped progress bar This feature will be used to implement the burn validation step. * Implement alert-ribbon CSS component This component will be used to inform an error situation to the user during the burn/check processes. * Add "Enable write validation on success" setting * Implement write validation support Signed-off-by: Juan Cruz Viotti <jviottidc@gmail.com> Fixes: https://github.com/resin-io/etcher/issues/45
This commit is contained in:
parent
e8516b1727
commit
3392a5eca1
@ -1254,7 +1254,7 @@ mark,
|
||||
.text-right, .section-header {
|
||||
text-align: right; }
|
||||
|
||||
.text-center {
|
||||
.text-center, .alert, .alert-ribbon {
|
||||
text-align: center; }
|
||||
|
||||
.text-justify {
|
||||
@ -3025,28 +3025,28 @@ fieldset[disabled] a.progress-button {
|
||||
|
||||
.btn-warning {
|
||||
color: #fff;
|
||||
background-color: #f0ad4e;
|
||||
border-color: #eea236; }
|
||||
background-color: #e99852;
|
||||
border-color: #e68b3b; }
|
||||
.btn-warning:focus, .btn-warning.focus {
|
||||
color: #fff;
|
||||
background-color: #ec971f;
|
||||
border-color: #985f0d; }
|
||||
background-color: #e37d25;
|
||||
border-color: #904c12; }
|
||||
.btn-warning:hover {
|
||||
color: #fff;
|
||||
background-color: #ec971f;
|
||||
border-color: #d58512; }
|
||||
background-color: #e37d25;
|
||||
border-color: #cb6c1a; }
|
||||
.btn-warning:active, .btn-warning.active,
|
||||
.open > .btn-warning.dropdown-toggle {
|
||||
color: #fff;
|
||||
background-color: #ec971f;
|
||||
border-color: #d58512; }
|
||||
background-color: #e37d25;
|
||||
border-color: #cb6c1a; }
|
||||
.btn-warning:active:hover, .btn-warning:active:focus, .btn-warning:active.focus, .btn-warning.active:hover, .btn-warning.active:focus, .btn-warning.active.focus,
|
||||
.open > .btn-warning.dropdown-toggle:hover,
|
||||
.open > .btn-warning.dropdown-toggle:focus,
|
||||
.open > .btn-warning.dropdown-toggle.focus {
|
||||
color: #fff;
|
||||
background-color: #d58512;
|
||||
border-color: #985f0d; }
|
||||
background-color: #cb6c1a;
|
||||
border-color: #904c12; }
|
||||
.btn-warning:active, .btn-warning.active,
|
||||
.open > .btn-warning.dropdown-toggle {
|
||||
background-image: none; }
|
||||
@ -3054,10 +3054,10 @@ fieldset[disabled] a.progress-button {
|
||||
fieldset[disabled] .btn-warning:hover,
|
||||
fieldset[disabled] .btn-warning:focus,
|
||||
fieldset[disabled] .btn-warning.focus {
|
||||
background-color: #f0ad4e;
|
||||
border-color: #eea236; }
|
||||
background-color: #e99852;
|
||||
border-color: #e68b3b; }
|
||||
.btn-warning .badge {
|
||||
color: #f0ad4e;
|
||||
color: #e99852;
|
||||
background-color: #fff; }
|
||||
|
||||
.btn-danger {
|
||||
@ -3124,7 +3124,7 @@ fieldset[disabled] a.progress-button {
|
||||
line-height: 1.33333;
|
||||
border-radius: 6px; }
|
||||
|
||||
.btn-sm, .btn-group-sm > .btn, .btn-group-sm > .progress-button {
|
||||
.btn-sm, .btn-group-sm > .btn, .btn-group-sm > .progress-button, .alert-ribbon .btn-link {
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
@ -3979,7 +3979,7 @@ tbody.collapse.in {
|
||||
.navbar-btn {
|
||||
margin-top: 9px;
|
||||
margin-bottom: 9px; }
|
||||
.navbar-btn.btn-sm, .btn-group-sm > .navbar-btn.btn, .btn-group-sm > .navbar-btn.progress-button {
|
||||
.navbar-btn.btn-sm, .btn-group-sm > .navbar-btn.btn, .btn-group-sm > .navbar-btn.progress-button, .alert-ribbon .navbar-btn.btn-link {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px; }
|
||||
.navbar-btn.btn-xs, .btn-group-xs > .navbar-btn.btn, .btn-group-xs > .navbar-btn.progress-button {
|
||||
@ -4305,9 +4305,9 @@ a.label:hover, a.label:focus {
|
||||
background-color: #31b0d5; }
|
||||
|
||||
.label-warning {
|
||||
background-color: #f0ad4e; }
|
||||
background-color: #e99852; }
|
||||
.label-warning[href]:hover, .label-warning[href]:focus {
|
||||
background-color: #ec971f; }
|
||||
background-color: #e37d25; }
|
||||
|
||||
.label-danger {
|
||||
background-color: #d9534f; }
|
||||
@ -4414,25 +4414,25 @@ a.thumbnail:focus,
|
||||
a.thumbnail.active {
|
||||
border-color: #ddd; }
|
||||
|
||||
.alert {
|
||||
padding: 15px;
|
||||
.alert, .alert-ribbon {
|
||||
padding: 13px;
|
||||
margin-bottom: 18px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px; }
|
||||
.alert h4 {
|
||||
.alert h4, .alert-ribbon h4 {
|
||||
margin-top: 0;
|
||||
color: inherit; }
|
||||
.alert .alert-link {
|
||||
.alert .alert-link, .alert-ribbon .alert-link {
|
||||
font-weight: bold; }
|
||||
.alert > p,
|
||||
.alert > ul {
|
||||
.alert > p, .alert-ribbon > p,
|
||||
.alert > ul, .alert-ribbon > ul {
|
||||
margin-bottom: 0; }
|
||||
.alert > p + p {
|
||||
.alert > p + p, .alert-ribbon > p + p {
|
||||
margin-top: 5px; }
|
||||
|
||||
.alert-dismissable,
|
||||
.alert-dismissible {
|
||||
padding-right: 35px; }
|
||||
padding-right: 33px; }
|
||||
.alert-dismissable .close,
|
||||
.alert-dismissible .close {
|
||||
position: relative;
|
||||
@ -4459,13 +4459,13 @@ a.thumbnail.active {
|
||||
color: #245269; }
|
||||
|
||||
.alert-warning {
|
||||
background-color: #fcf8e3;
|
||||
background-color: #e99852;
|
||||
border-color: #faebcc;
|
||||
color: #8a6d3b; }
|
||||
color: #fff; }
|
||||
.alert-warning hr {
|
||||
border-top-color: #f7e1b5; }
|
||||
.alert-warning .alert-link {
|
||||
color: #66512c; }
|
||||
color: #e6e6e6; }
|
||||
|
||||
.alert-danger {
|
||||
background-color: #f2dede;
|
||||
@ -4540,7 +4540,7 @@ a.thumbnail.active {
|
||||
background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); }
|
||||
|
||||
.progress-bar-warning {
|
||||
background-color: #f0ad4e; }
|
||||
background-color: #e99852; }
|
||||
.progress-striped .progress-bar-warning {
|
||||
background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
|
||||
background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
|
||||
@ -5915,6 +5915,10 @@ html {
|
||||
.modal-backdrop.in {
|
||||
opacity: 0; }
|
||||
|
||||
.alert, .alert-ribbon {
|
||||
border: none;
|
||||
border-radius: 2px; }
|
||||
|
||||
/*
|
||||
* Copyright 2016 Resin.io
|
||||
*
|
||||
@ -6032,7 +6036,7 @@ html {
|
||||
.btn-brick {
|
||||
min-width: 170px; }
|
||||
|
||||
.btn-sm, .btn-group-sm > .btn, .btn-group-sm > .progress-button {
|
||||
.btn-sm, .btn-group-sm > .btn, .btn-group-sm > .progress-button, .alert-ribbon .btn-link {
|
||||
font-size: 10px;
|
||||
padding: 4px 12px; }
|
||||
|
||||
@ -6143,6 +6147,13 @@ button.btn:focus, button.progress-button:focus {
|
||||
* This is useful to determine if the progress bar is paused from the point of view
|
||||
* of the styling.
|
||||
*
|
||||
* You can optionally pass the `.progress-button--striped` modified to get a striped
|
||||
* progress bar.
|
||||
*
|
||||
* The stripe implementation idea was taken from:
|
||||
*
|
||||
* https://css-tricks.com/css3-progress-bars/
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* <button class="progress-button progress-button--primary" percentage="50" active="true">
|
||||
@ -6153,11 +6164,14 @@ button.btn:focus, button.progress-button:focus {
|
||||
.progress-button--primary .progress-button__bar {
|
||||
background: #6ca1e0; }
|
||||
|
||||
.progress-button--primary.progress-button--striped .progress-button__bar:after {
|
||||
background-image: -webkit-gradient(linear, 0 0, 100% 100%, color-stop(0.25, #3b679b), color-stop(0.25, #5c93d6), color-stop(0.5, #5c93d6), color-stop(0.5, #3b679b), color-stop(0.75, #3b679b), color-stop(0.75, #5c93d6), to(#5c93d6)); }
|
||||
|
||||
.progress-button[percentage="100"][active="false"] .progress-button__bar {
|
||||
background-color: #5cb85c; }
|
||||
|
||||
.progress-button[percentage="100"][active="true"] .progress-button__bar {
|
||||
background-color: #f0ad4e; }
|
||||
background-color: #e99852; }
|
||||
|
||||
.progress-button[active="true"] {
|
||||
pointer-events: none; }
|
||||
@ -6172,7 +6186,24 @@ button.btn:focus, button.progress-button:focus {
|
||||
top: 0;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
transition: width 0.3s, opacity 0.3s; }
|
||||
transition: width 0.3s; }
|
||||
|
||||
.progress-button--striped .progress-button__bar:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-size: 20px 20px;
|
||||
animation: progress-button-stripes 1s linear infinite;
|
||||
overflow: hidden; }
|
||||
|
||||
@keyframes progress-button-stripes {
|
||||
0% {
|
||||
background-position: 0 0; }
|
||||
100% {
|
||||
background-position: 20px 20px; } }
|
||||
|
||||
/*
|
||||
* Copyright 2016 Resin.io
|
||||
@ -6237,6 +6268,51 @@ button.btn:focus, button.progress-button:focus {
|
||||
.modal-body .list-group-item:first-child {
|
||||
border-top: none; }
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
.alert-ribbon {
|
||||
width: 60%;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
top: -100%;
|
||||
transition: top 0.5s; }
|
||||
.alert-ribbon > .glyphicon:first-child, .alert-ribbon > .tick:first-child {
|
||||
margin-right: 5px; }
|
||||
.alert-ribbon > .glyphicon:last-child, .alert-ribbon > .tick:last-child {
|
||||
margin-left: 5px; }
|
||||
.alert-ribbon .btn-link {
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
vertical-align: baseline;
|
||||
border-radius: 0;
|
||||
border-bottom: 1px solid; }
|
||||
.alert-ribbon.alert-warning .btn-link {
|
||||
border-color: #f7dbc3;
|
||||
color: #fff; }
|
||||
.alert-ribbon.alert-warning .btn-link:hover {
|
||||
color: #e6e6e6;
|
||||
border-color: #f2c096; }
|
||||
|
||||
.alert-ribbon--open {
|
||||
top: 0; }
|
||||
|
||||
hero-icon[disabled] .caption {
|
||||
color: #787c7f; }
|
||||
|
||||
|
@ -54,6 +54,7 @@ const app = angular.module('Etcher', [
|
||||
|
||||
// Models
|
||||
'Etcher.Models.SelectionState',
|
||||
'Etcher.Models.Settings',
|
||||
|
||||
// Components
|
||||
'Etcher.Components.ProgressButton',
|
||||
@ -87,6 +88,7 @@ app.controller('AppController', function(
|
||||
NotifierService,
|
||||
DriveScannerService,
|
||||
SelectionStateModel,
|
||||
SettingsModel,
|
||||
ImageWriterService,
|
||||
AnalyticsService,
|
||||
DriveSelectorService,
|
||||
@ -96,6 +98,8 @@ app.controller('AppController', function(
|
||||
this.selection = SelectionStateModel;
|
||||
this.writer = ImageWriterService;
|
||||
this.scanner = DriveScannerService;
|
||||
this.settings = SettingsModel.data;
|
||||
this.success = true;
|
||||
|
||||
// This catches the case where the user enters
|
||||
// the settings screen when a burn finished
|
||||
@ -114,7 +118,7 @@ app.controller('AppController', function(
|
||||
}
|
||||
|
||||
NotifierService.subscribe($scope, 'image-writer:state', function(state) {
|
||||
AnalyticsService.log(`Progress: ${state.progress}% at ${state.speed} MB/s`);
|
||||
AnalyticsService.log(`Progress (${state.type}): ${state.progress}% at ${state.speed} MB/s`);
|
||||
WindowProgressService.set(state.progress);
|
||||
});
|
||||
|
||||
@ -202,6 +206,16 @@ app.controller('AppController', function(
|
||||
AnalyticsService.logEvent('Reselect drive');
|
||||
};
|
||||
|
||||
this.restartAfterFailure = function() {
|
||||
self.selection.clear({
|
||||
preserveImage: true
|
||||
});
|
||||
|
||||
self.writer.resetState();
|
||||
self.success = true;
|
||||
AnalyticsService.logEvent('Restart after failure');
|
||||
};
|
||||
|
||||
this.burn = function(image, drive) {
|
||||
|
||||
// Stop scanning drives when burning
|
||||
@ -213,9 +227,18 @@ app.controller('AppController', function(
|
||||
device: drive.device
|
||||
});
|
||||
|
||||
return self.writer.burn(image, drive).then(function() {
|
||||
return self.writer.burn(image, drive).then(function(success) {
|
||||
|
||||
// TODO: Find a better way to manage burn/check
|
||||
// success/error state than a global boolean flag.
|
||||
self.success = success;
|
||||
|
||||
if (self.success) {
|
||||
AnalyticsService.logEvent('Done');
|
||||
$state.go('success');
|
||||
} else {
|
||||
AnalyticsService.logEvent('Burn error');
|
||||
}
|
||||
})
|
||||
.catch(dialog.showError)
|
||||
.finally(WindowProgressService.clear);
|
||||
|
@ -22,7 +22,7 @@
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* <progress-button percentage="{{ 40 }}">My Progress Button</progress-button>
|
||||
* <progress-button percentage="{{ 40 }}" striped>My Progress Button</progress-button>
|
||||
*/
|
||||
|
||||
module.exports = function() {
|
||||
@ -32,7 +32,8 @@ module.exports = function() {
|
||||
replace: true,
|
||||
transclude: true,
|
||||
scope: {
|
||||
percentage: '='
|
||||
percentage: '=',
|
||||
striped: '@'
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -29,6 +29,13 @@
|
||||
* This is useful to determine if the progress bar is paused from the point of view
|
||||
* of the styling.
|
||||
*
|
||||
* You can optionally pass the `.progress-button--striped` modified to get a striped
|
||||
* progress bar.
|
||||
*
|
||||
* The stripe implementation idea was taken from:
|
||||
*
|
||||
* https://css-tricks.com/css3-progress-bars/
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* <button class="progress-button progress-button--primary" percentage="50" active="true">
|
||||
@ -37,6 +44,9 @@
|
||||
* </button>
|
||||
*/
|
||||
|
||||
$progress-button-stripes-width: 20px;
|
||||
$progress-button-stripes-animation-duration: 1s;
|
||||
|
||||
.progress-button {
|
||||
@extend .btn;
|
||||
}
|
||||
@ -47,6 +57,20 @@
|
||||
.progress-button__bar {
|
||||
background: lighten($brand-primary, 5);
|
||||
}
|
||||
|
||||
$progress-button-stripes-background-color: desaturate($brand-primary, 5%);
|
||||
$progress-button-stripes-color: desaturate(darken($brand-primary, 18%), 20%);
|
||||
|
||||
&.progress-button--striped .progress-button__bar:after {
|
||||
background-image: -webkit-gradient(linear, 0 0, 100% 100%,
|
||||
color-stop(0.25, $progress-button-stripes-color),
|
||||
color-stop(0.25, $progress-button-stripes-background-color),
|
||||
color-stop(0.50, $progress-button-stripes-background-color),
|
||||
color-stop(0.50, $progress-button-stripes-color),
|
||||
color-stop(0.75, $progress-button-stripes-color),
|
||||
color-stop(0.75, $progress-button-stripes-background-color),
|
||||
to($progress-button-stripes-background-color));
|
||||
}
|
||||
}
|
||||
|
||||
.progress-button[percentage="100"][active="false"] .progress-button__bar {
|
||||
@ -77,6 +101,32 @@
|
||||
height: 100%;
|
||||
|
||||
// Subtle progress bar animation
|
||||
transition: width 0.3s, opacity 0.3s;
|
||||
transition: width 0.3s;
|
||||
|
||||
}
|
||||
|
||||
.progress-button--striped .progress-button__bar:after {
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
|
||||
background-size: $progress-button-stripes-width $progress-button-stripes-width;
|
||||
animation: progress-button-stripes $progress-button-stripes-animation-duration linear infinite;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@keyframes progress-button-stripes {
|
||||
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: $progress-button-stripes-width $progress-button-stripes-width;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,7 @@
|
||||
<button class="progress-button progress-button--primary">
|
||||
<button class="progress-button progress-button--primary"
|
||||
ng-class="{
|
||||
'progress-button--striped': striped && striped != 'false'
|
||||
}">
|
||||
<span class="progress-button__content" ng-transclude></span>
|
||||
<span class="progress-button__bar" ng-style="{ width: percentage + '%' }"></span>
|
||||
</button>
|
||||
|
@ -35,7 +35,8 @@ SettingsModel.service('SettingsModel', function($localStorage) {
|
||||
*/
|
||||
this.data = $localStorage.$default({
|
||||
errorReporting: true,
|
||||
unmountOnSuccess: true
|
||||
unmountOnSuccess: true,
|
||||
validateWriteOnSuccess: true
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -154,6 +154,7 @@ imageWriter.service('ImageWriterService', function($q, $timeout, SettingsModel,
|
||||
$timeout(function() {
|
||||
|
||||
self.state = {
|
||||
type: state.type,
|
||||
progress: Math.floor(state.percentage),
|
||||
|
||||
// Transform bytes to megabytes preserving only two decimal places
|
||||
|
@ -14,4 +14,11 @@
|
||||
<span>Enable auto-unmounting on success</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="settings.storage.validateWriteOnSuccess">
|
||||
<span>Enable write validation on success</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -50,17 +50,39 @@
|
||||
<span class="badge space-top-medium" ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()">3</span>
|
||||
|
||||
<div class="space-vertical-large">
|
||||
<progress-button class="btn-brick" percentage="app.writer.state.progress" ng-attr-active="{{ app.writer.isBurning() }}"
|
||||
<progress-button class="btn-brick"
|
||||
percentage="app.writer.state.progress"
|
||||
striped="{{ app.writer.state.type == 'check' }}"
|
||||
ng-attr-active="{{ app.writer.isBurning() }}"
|
||||
ng-show="app.success"
|
||||
ng-click="app.burn(app.selection.getImage(), app.selection.getDrive())"
|
||||
ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()">
|
||||
<span ng-show="app.writer.state.progress == 100 && app.writer.isBurning()">Finishing...</span>
|
||||
<span ng-show="app.writer.state.progress == 0 && !app.writer.isBurning()">Burn!</span>
|
||||
<span ng-show="app.writer.state.progress == 0 && app.writer.isBurning() && !app.writer.state.speed">Starting...</span>
|
||||
<span ng-show="app.writer.state.speed && app.writer.state.progress != 100"
|
||||
<span ng-show="app.writer.state.speed && app.writer.state.progress != 100 && app.writer.state.type != 'check'"
|
||||
ng-bind="app.writer.state.progress + '% '"></span>
|
||||
<span ng-show="app.writer.state.speed && app.writer.state.progress != 100 && app.writer.state.type == 'check'"
|
||||
ng-bind="app.writer.state.progress + '% Validating...'"></span>
|
||||
</progress-button>
|
||||
|
||||
<p class="step-footer" ng-bind="app.writer.state.speed.toFixed(2) + ' MB/s'" ng-show="app.writer.state.speed && app.writer.state.progress != 100"></p>
|
||||
<div class="alert-ribbon alert-warning" ng-class="{ 'alert-ribbon--open': !app.success }">
|
||||
<span class="glyphicon glyphicon-warning-sign"></span>
|
||||
<span ng-show="app.settings.validateWriteOnSuccess">
|
||||
Your removable drive did not pass validation check.<br>Please insert another one and <button class="btn btn-link" ng-click="app.restartAfterFailure()">try again</button>
|
||||
</span>
|
||||
<span ng-hide="app.settings.validateWriteOnSuccess">
|
||||
Oops, seems something went wrong. Click <button class="btn btn-link" ng-click="app.restartAfterFailure()">here</button> to retry
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-warning btn-brick" ng-hide="app.success" ng-click="app.restartAfterFailure()">
|
||||
<span class="glyphicon glyphicon-repeat"></span> Retry
|
||||
</button>
|
||||
|
||||
<p class="step-footer"
|
||||
ng-bind="app.writer.state.speed.toFixed(2) + ' MB/s'"
|
||||
ng-show="app.writer.state.speed && app.writer.state.progress != 100 && app.writer.state.type == 'write'"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
68
lib/scss/components/_alert-ribbon.scss
Normal file
68
lib/scss/components/_alert-ribbon.scss
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.alert-ribbon {
|
||||
@extend .alert;
|
||||
|
||||
width: 60%;
|
||||
position: fixed;
|
||||
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
|
||||
// Animate appearance
|
||||
top: -100%;
|
||||
transition: top 0.5s;
|
||||
|
||||
// Align alert icons a bit better
|
||||
> .glyphicon {
|
||||
&:first-child {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
@extend .btn-sm;
|
||||
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
vertical-align: baseline;
|
||||
border-radius: 0;
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
|
||||
&.alert-warning .btn-link {
|
||||
border-color: lighten($alert-warning-bg, 25%);
|
||||
color: $alert-warning-text;
|
||||
|
||||
&:hover {
|
||||
color: darken($alert-warning-text, 10%);
|
||||
border-color: lighten($alert-warning-bg, 15%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alert-ribbon--open {
|
||||
top: 0;
|
||||
}
|
@ -28,6 +28,10 @@ $link-color: $gray-light;
|
||||
$link-hover-decoration: none;
|
||||
$btn-default-bg: #ececec;
|
||||
$btn-default-color: #b3b3b3;
|
||||
$brand-warning: rgb(233, 152, 82);
|
||||
$alert-warning-bg: $brand-warning;
|
||||
$alert-warning-text: #fff;
|
||||
$alert-padding: 13px;
|
||||
|
||||
@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap";
|
||||
|
||||
@ -39,6 +43,7 @@ $btn-default-color: #b3b3b3;
|
||||
@import "./components/tick";
|
||||
@import "../browser/components/progress-button/styles/progress-button";
|
||||
@import "./components/modal";
|
||||
@import "./components/alert-ribbon";
|
||||
|
||||
hero-icon[disabled] .caption {
|
||||
color: $color-disabled;
|
||||
|
@ -36,3 +36,10 @@ html {
|
||||
.modal-backdrop.in {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.alert {
|
||||
@extend .text-center;
|
||||
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
@ -70,15 +70,18 @@ exports.getImageStream = function(image) {
|
||||
* @param {Object} drive - drive
|
||||
* @param {Object} options - options
|
||||
* @param {Boolean} [options.unmountOnSuccess=false] - unmount on success
|
||||
* @param {Boolean} [options.validateWriteOnSuccess=false] - validate write on success
|
||||
* @param {Function} onProgress - on progress callback (state)
|
||||
*
|
||||
* @fulfil {Boolean} - whether the operation was successful
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* writer.writeImage('path/to/image.img', {
|
||||
* device: '/dev/disk2'
|
||||
* }, {
|
||||
* unmountOnSuccess: true
|
||||
* unmountOnSuccess: true,
|
||||
* validateWriteOnSuccess: true
|
||||
* }, function(state) {
|
||||
* console.log(state.percentage);
|
||||
* }).then(function() {
|
||||
@ -89,14 +92,16 @@ exports.writeImage = function(image, drive, options, onProgress) {
|
||||
return umount.umountAsync(drive.device).then(function() {
|
||||
return exports.getImageStream(image);
|
||||
}).then(function(stream) {
|
||||
return imageWrite.write(drive.device, stream);
|
||||
return imageWrite.write(drive.device, stream, {
|
||||
check: options.validateWriteOnSuccess
|
||||
});
|
||||
}).then(function(writer) {
|
||||
return new Bluebird(function(resolve, reject) {
|
||||
writer.on('progress', onProgress);
|
||||
writer.on('error', reject);
|
||||
writer.on('done', resolve);
|
||||
});
|
||||
}).then(function() {
|
||||
}).tap(function() {
|
||||
if (!options.unmountOnSuccess) {
|
||||
return;
|
||||
}
|
||||
|
@ -84,7 +84,7 @@
|
||||
"lodash": "^4.5.1",
|
||||
"ngstorage": "^0.3.10",
|
||||
"open": "0.0.5",
|
||||
"resin-image-write": "^2.0.5",
|
||||
"resin-image-write": "^3.0.2",
|
||||
"resin-zip-image": "^1.1.2",
|
||||
"sudo-prompt": "^2.2.0",
|
||||
"trackjs": "^2.1.16",
|
||||
|
Loading…
x
Reference in New Issue
Block a user