From 7e0f54e7d6f440614385eb3141b5d2cd7e40ffb3 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 26 Jul 2016 08:47:20 -0400 Subject: [PATCH] feat(GUI): rich image extensions (#597) The following PRs add support for a custom `_info` directory containing metadata such an image URL, display name, logo, etc in `etcher-image-stream`: - https://github.com/resin-io-modules/etcher-image-stream/pull/16 - https://github.com/resin-io-modules/etcher-image-stream/pull/14 - https://github.com/resin-io-modules/etcher-image-stream/pull/13 Now that this module supports such metadata, we make use of it in the GUI as follows: - The file name is replaced with the display name. - The file name links to the image URL. - The "Select Image" logo is replaced with the image logo. Some miscellaneous changes introduces in this PR to support the changes described above: - Implement `SelectionStateModel.getImageUrl()`. - Implement `SelectionStateModel.getImageLogo()`. - Implement `SelectionStateModel.getImageName()`. - Ignore the "logo" image property when displaying the "Select image" event, in order to not fill the console with SVG contents. - Make `svg-icon` understand SVG strings as paths. - Make `svg-icon` react to changes to the `path` attribute. - Extract the core functionality of `openExternal` into `OSOpenExternalService`. - Upgrade `etcher-image-stream` to v3.0.1. Change-Type: minor Changelog-Entry: Support rich image extensions. Fixes: https://github.com/resin-io/etcher/issues/470 Signed-off-by: Juan Cruz Viotti --- lib/gui/app.js | 13 +- .../svg-icon/directives/svg-icon.js | 36 ++- lib/gui/models/selection-state.js | 42 +++ lib/gui/models/store.js | 12 + lib/gui/os/dialog/services/dialog.js | 7 +- .../open-external/directives/open-external.js | 7 +- lib/gui/os/open-external/open-external.js | 1 + .../open-external/services/open-external.js | 35 +++ lib/gui/partials/main.html | 5 +- npm-shrinkwrap.json | 257 +++++++++++++++++- package.json | 2 +- tests/gui/components/svg-icon.spec.js | 25 +- tests/gui/models/selection-state.spec.js | 74 ++++- tests/gui/os/open-external.spec.js | 28 +- 14 files changed, 494 insertions(+), 50 deletions(-) create mode 100644 lib/gui/os/open-external/services/open-external.js diff --git a/lib/gui/app.js b/lib/gui/app.js index 0e1bdd13..695fbcb4 100644 --- a/lib/gui/app.js +++ b/lib/gui/app.js @@ -146,7 +146,8 @@ app.controller('AppController', function( TooltipModalService, OSWindowProgressService, OSNotificationService, - OSDialogService + OSDialogService, + OSOpenExternalService ) { this.formats = SupportedFormatsModel; this.selection = SelectionStateModel; @@ -205,7 +206,15 @@ app.controller('AppController', function( } this.selection.setImage(image); - AnalyticsService.logEvent('Select image', image); + AnalyticsService.logEvent('Select image', _.omit(image, 'logo')); + }; + + this.openImageUrl = () => { + const imageUrl = this.selection.getImageUrl(); + + if (imageUrl) { + OSOpenExternalService.open(imageUrl); + } }; this.openImageSelector = () => { diff --git a/lib/gui/components/svg-icon/directives/svg-icon.js b/lib/gui/components/svg-icon/directives/svg-icon.js index 895e0175..0039904c 100644 --- a/lib/gui/components/svg-icon/directives/svg-icon.js +++ b/lib/gui/components/svg-icon/directives/svg-icon.js @@ -16,6 +16,7 @@ 'use strict'; +const _ = require('lodash'); const path = require('path'); const fs = require('fs'); @@ -45,21 +46,30 @@ module.exports = () => { height: '@' }, link: (scope, element) => { - - // This means the path to the icon should be - // relative to *this directory*. - // TODO: There might be a way to compute the path - // relatively to the `index.html`. - const imagePath = path.join(__dirname, scope.path); - - const contents = fs.readFileSync(imagePath, { - encoding: 'utf8' - }); - - element.html(contents); - element.css('width', scope.width || '40px'); element.css('height', scope.height || '40px'); + + scope.$watch('path', (value) => { + + // The path contains SVG contents + if (_.first(value) === '<') { + element.html(value); + + } else { + + // This means the path to the icon should be + // relative to *this directory*. + // TODO: There might be a way to compute the path + // relatively to the `index.html`. + const imagePath = path.join(__dirname, value); + + const contents = fs.readFileSync(imagePath, { + encoding: 'utf8' + }); + + element.html(contents); + } + }); } }; }; diff --git a/lib/gui/models/selection-state.js b/lib/gui/models/selection-state.js index 25477c50..a0c0805e 100644 --- a/lib/gui/models/selection-state.js +++ b/lib/gui/models/selection-state.js @@ -220,6 +220,48 @@ SelectionStateModel.service('SelectionStateModel', function() { return _.get(Store.getState().toJS(), 'selection.image.size'); }; + /** + * @summary Get image url + * @function + * @public + * + * @returns {String} image url + * + * @example + * const imageUrl = SelectionStateModel.getImageUrl(); + */ + this.getImageUrl = () => { + return _.get(Store.getState().toJS(), 'selection.image.url'); + }; + + /** + * @summary Get image name + * @function + * @public + * + * @returns {String} image name + * + * @example + * const imageName = SelectionStateModel.getImageName(); + */ + this.getImageName = () => { + return _.get(Store.getState().toJS(), 'selection.image.name'); + }; + + /** + * @summary Get image logo + * @function + * @public + * + * @returns {String} image logo + * + * @example + * const imageLogo = SelectionStateModel.getImageLogo(); + */ + this.getImageLogo = () => { + return _.get(Store.getState().toJS(), 'selection.image.logo'); + }; + /** * @summary Check if there is a selected drive * @function diff --git a/lib/gui/models/store.js b/lib/gui/models/store.js index fd1923f1..46a467ce 100644 --- a/lib/gui/models/store.js +++ b/lib/gui/models/store.js @@ -274,6 +274,18 @@ const storeReducer = (state, action) => { throw new Error(`Invalid image size: ${action.data.size}`); } + if (action.data.url && !_.isString(action.data.url)) { + throw new Error(`Invalid image url: ${action.data.url}`); + } + + if (action.data.name && !_.isString(action.data.name)) { + throw new Error(`Invalid image name: ${action.data.name}`); + } + + if (action.data.logo && !_.isString(action.data.logo)) { + throw new Error(`Invalid image logo: ${action.data.logo}`); + } + return state.setIn([ 'selection', 'image' ], Immutable.fromJS(action.data)); } diff --git a/lib/gui/os/dialog/services/dialog.js b/lib/gui/os/dialog/services/dialog.js index d064e6fe..76ee5156 100644 --- a/lib/gui/os/dialog/services/dialog.js +++ b/lib/gui/os/dialog/services/dialog.js @@ -71,10 +71,13 @@ module.exports = function($q, SupportedFormatsModel) { return resolve(); } - imageStream.getEstimatedFinalSize(imagePath).then((estimatedSize) => { + imageStream.getImageMetadata(imagePath).then((metadata) => { return resolve({ path: imagePath, - size: estimatedSize + size: metadata.estimatedSize, + name: metadata.name, + url: metadata.url, + logo: metadata.logo }); }).catch(reject); }); diff --git a/lib/gui/os/open-external/directives/open-external.js b/lib/gui/os/open-external/directives/open-external.js index 7a455e1b..b43a8d96 100644 --- a/lib/gui/os/open-external/directives/open-external.js +++ b/lib/gui/os/open-external/directives/open-external.js @@ -16,8 +16,6 @@ 'use strict'; -const electron = require('electron'); - /** * @summary OsOpenExternal directive * @function @@ -27,12 +25,13 @@ const electron = require('electron'); * This directive provides an attribute to open an external * resource with the default operating system action. * + * @param {Object} OSOpenExternalService - OSOpenExternalService * @returns {Object} directive * * @example * */ -module.exports = () => { +module.exports = (OSOpenExternalService) => { return { restrict: 'A', scope: false, @@ -43,7 +42,7 @@ module.exports = () => { element.css('cursor', 'pointer'); element.on('click', () => { - electron.shell.openExternal(attributes.osOpenExternal); + OSOpenExternalService.open(attributes.osOpenExternal); }); } }; diff --git a/lib/gui/os/open-external/open-external.js b/lib/gui/os/open-external/open-external.js index ebb25cae..1d6bcb0a 100644 --- a/lib/gui/os/open-external/open-external.js +++ b/lib/gui/os/open-external/open-external.js @@ -23,6 +23,7 @@ const angular = require('angular'); const MODULE_NAME = 'Etcher.OS.OpenExternal'; const OSOpenExternal = angular.module(MODULE_NAME, []); +OSOpenExternal.service('OSOpenExternalService', require('./services/open-external')); OSOpenExternal.directive('osOpenExternal', require('./directives/open-external')); module.exports = MODULE_NAME; diff --git a/lib/gui/os/open-external/services/open-external.js b/lib/gui/os/open-external/services/open-external.js new file mode 100644 index 00000000..1b9f5e92 --- /dev/null +++ b/lib/gui/os/open-external/services/open-external.js @@ -0,0 +1,35 @@ +/* + * 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'); + +module.exports = function() { + + /** + * @summary Open an external resource + * @function + * @public + * + * @param {String} url - url + * + * @example + * OSOpenExternalService.open('https://www.google.com'); + */ + this.open = electron.shell.openExternal; + +}; diff --git a/lib/gui/partials/main.html b/lib/gui/partials/main.html index 8be4912b..7f0d62f1 100644 --- a/lib/gui/partials/main.html +++ b/lib/gui/partials/main.html @@ -1,7 +1,7 @@
- + SELECT IMAGE 1 @@ -16,7 +16,8 @@

-
+