From 3392a5eca18e17a6eebd3a3bacec74cd40cf5657 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Wed, 6 Apr 2016 21:53:35 -0400 Subject: [PATCH] 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 Fixes: https://github.com/resin-io/etcher/issues/45 --- build/css/main.css | 142 ++++++++++++++---- lib/browser/app.js | 31 +++- .../directives/progress-button.js | 5 +- .../styles/_progress-button.scss | 52 ++++++- .../templates/progress-button.tpl.html | 9 +- lib/browser/models/settings.js | 3 +- lib/browser/modules/image-writer.js | 1 + .../settings/templates/settings.tpl.html | 7 + lib/partials/main.html | 28 +++- lib/scss/components/_alert-ribbon.scss | 68 +++++++++ lib/scss/main.scss | 5 + lib/scss/modules/_bootstrap.scss | 7 + lib/src/writer.js | 11 +- package.json | 2 +- 14 files changed, 320 insertions(+), 51 deletions(-) create mode 100644 lib/scss/components/_alert-ribbon.scss diff --git a/build/css/main.css b/build/css/main.css index de9c239b..c512fbf2 100644 --- a/build/css/main.css +++ b/build/css/main.css @@ -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: * * */ +$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; + } } diff --git a/lib/browser/components/progress-button/templates/progress-button.tpl.html b/lib/browser/components/progress-button/templates/progress-button.tpl.html index 38d3a493..ab019a95 100644 --- a/lib/browser/components/progress-button/templates/progress-button.tpl.html +++ b/lib/browser/components/progress-button/templates/progress-button.tpl.html @@ -1,4 +1,7 @@ - diff --git a/lib/browser/models/settings.js b/lib/browser/models/settings.js index 3cac9184..c6c6d532 100644 --- a/lib/browser/models/settings.js +++ b/lib/browser/models/settings.js @@ -35,7 +35,8 @@ SettingsModel.service('SettingsModel', function($localStorage) { */ this.data = $localStorage.$default({ errorReporting: true, - unmountOnSuccess: true + unmountOnSuccess: true, + validateWriteOnSuccess: true }); }); diff --git a/lib/browser/modules/image-writer.js b/lib/browser/modules/image-writer.js index 9ad1d963..21719e1d 100644 --- a/lib/browser/modules/image-writer.js +++ b/lib/browser/modules/image-writer.js @@ -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 diff --git a/lib/browser/pages/settings/templates/settings.tpl.html b/lib/browser/pages/settings/templates/settings.tpl.html index f08bebdc..e726b6ff 100644 --- a/lib/browser/pages/settings/templates/settings.tpl.html +++ b/lib/browser/pages/settings/templates/settings.tpl.html @@ -14,4 +14,11 @@ Enable auto-unmounting on success + +
+ +
diff --git a/lib/partials/main.html b/lib/partials/main.html index 06901993..55473551 100644 --- a/lib/partials/main.html +++ b/lib/partials/main.html @@ -50,17 +50,39 @@ 3
- Finishing... Burn! Starting... - + - +
+ + + Your removable drive did not pass validation check.
Please insert another one and +
+ + Oops, seems something went wrong. Click to retry + +
+ + + +
diff --git a/lib/scss/components/_alert-ribbon.scss b/lib/scss/components/_alert-ribbon.scss new file mode 100644 index 00000000..2940de39 --- /dev/null +++ b/lib/scss/components/_alert-ribbon.scss @@ -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; +} diff --git a/lib/scss/main.scss b/lib/scss/main.scss index 2d5847f3..aeebb897 100644 --- a/lib/scss/main.scss +++ b/lib/scss/main.scss @@ -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; diff --git a/lib/scss/modules/_bootstrap.scss b/lib/scss/modules/_bootstrap.scss index 9180a92f..b31e0893 100644 --- a/lib/scss/modules/_bootstrap.scss +++ b/lib/scss/modules/_bootstrap.scss @@ -36,3 +36,10 @@ html { .modal-backdrop.in { opacity: 0; } + +.alert { + @extend .text-center; + + border: none; + border-radius: 2px; +} diff --git a/lib/src/writer.js b/lib/src/writer.js index fb9b2b49..3d36aefe 100644 --- a/lib/src/writer.js +++ b/lib/src/writer.js @@ -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; } diff --git a/package.json b/package.json index 74e0a403..ef1ae550 100644 --- a/package.json +++ b/package.json @@ -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",