diff --git a/lib/gui/app/components/featured-project/featured-project.jsx b/lib/gui/app/components/featured-project/featured-project.jsx index b813197b..0fca321b 100644 --- a/lib/gui/app/components/featured-project/featured-project.jsx +++ b/lib/gui/app/components/featured-project/featured-project.jsx @@ -17,28 +17,25 @@ 'use strict' const React = require('react') +const propTypes = require('prop-types') const SafeWebview = require('../safe-webview/safe-webview.jsx') const settings = require('../../models/settings') const analytics = require('../../modules/analytics') -const endpoint = null class FeaturedProject extends React.Component { constructor (props) { super(props) this.state = { - endpoint + endpoint: null } } componentDidMount () { return settings.load() .then(() => { - if (settings.has('featuredProjectEndpoint')) { - this.setState({ endpoint: settings.get('featuredProjectEndpoint') }) - } else { - this.setState({ endpoint: 'https://assets.balena.io/etcher-featured/index.html' }) - } + const endpoint = settings.get('featuredProjectEndpoint') || 'https://assets.balena.io/etcher-featured/index.html' + this.setState({ endpoint }) }) .catch(analytics.logException) } @@ -53,4 +50,8 @@ class FeaturedProject extends React.Component { } } +FeaturedProject.propTypes = { + onWebviewShow: propTypes.func +} + module.exports = FeaturedProject diff --git a/lib/gui/app/components/reduced-flashing-infos/index.js b/lib/gui/app/components/reduced-flashing-infos/index.js new file mode 100644 index 00000000..4b1446e3 --- /dev/null +++ b/lib/gui/app/components/reduced-flashing-infos/index.js @@ -0,0 +1,34 @@ +/* + * 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.Components.ReducedFlashingInfos + */ + +const angular = require('angular') +const { react2angular } = require('react2angular') + +const MODULE_NAME = 'Etcher.Components.ReducedFlashingInfos' +const ReducedFlashingInfos = angular.module(MODULE_NAME, []) + +ReducedFlashingInfos.component( + 'reducedFlashingInfos', + react2angular(require('./reduced-flashing-infos.jsx')) +) + +module.exports = MODULE_NAME diff --git a/lib/gui/app/components/reduced-flashing-infos/reduced-flashing-infos.jsx b/lib/gui/app/components/reduced-flashing-infos/reduced-flashing-infos.jsx new file mode 100644 index 00000000..c9b2a2b5 --- /dev/null +++ b/lib/gui/app/components/reduced-flashing-infos/reduced-flashing-infos.jsx @@ -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. + */ + +'use strict' + +const React = require('react') +const propTypes = require('prop-types') +const styled = require('styled-components').default +const SvgIcon = require('../svg-icon/svg-icon.jsx') + +const Div = styled.div ` + position: absolute; + top: 45px; + left: 550px; + + > span.step-name { + justify-content: flex-start; + + > span { + margin-left: 5px; + font-weight: normal; + } + } + + .svg-icon[disabled] { + opacity: 0.4; + } +` + +const ReducedFlashingInfos = (props) => { + return (props.shouldShow) ? ( +
+ + + { props.imageName } + { props.imageSize } + + + + + { props.driveTitle } + +
+ ) : null +} + +ReducedFlashingInfos.propTypes = { + imageLogo: propTypes.string, + imageName: propTypes.string, + imageSize: propTypes.string, + driveTitle: propTypes.string, + shouldShow: propTypes.bool +} + +module.exports = ReducedFlashingInfos diff --git a/lib/gui/app/components/safe-webview/safe-webview.jsx b/lib/gui/app/components/safe-webview/safe-webview.jsx index d30d6ba1..2725c1c7 100644 --- a/lib/gui/app/components/safe-webview/safe-webview.jsx +++ b/lib/gui/app/components/safe-webview/safe-webview.jsx @@ -202,6 +202,9 @@ class SafeWebview extends react.PureComponent { this.setState({ shouldShow: event.httpResponseCode === HTTP_OK }) + if (this.props.onWebviewShow) { + this.props.onWebviewShow(event.httpResponseCode === HTTP_OK) + } } } @@ -265,7 +268,12 @@ SafeWebview.propTypes = { /** * @summary Refresh the webview */ - refreshNow: propTypes.bool + refreshNow: propTypes.bool, + + /** + * @summary Webview lifecycle event + */ + onWebviewShow: propTypes.func } diff --git a/lib/gui/app/components/svg-icon.js b/lib/gui/app/components/svg-icon.js index 3398d5cc..d00e5646 100644 --- a/lib/gui/app/components/svg-icon.js +++ b/lib/gui/app/components/svg-icon.js @@ -22,162 +22,11 @@ * @module Etcher.Components.SVGIcon */ -const _ = require('lodash') const angular = require('angular') -const react = require('react') -const propTypes = require('prop-types') const react2angular = require('react2angular').react2angular -const path = require('path') -const fs = require('fs') -const analytics = require('../modules/analytics') const MODULE_NAME = 'Etcher.Components.SVGIcon' const angularSVGIcon = angular.module(MODULE_NAME, []) -const DEFAULT_SIZE = '40px' - -const domParser = new window.DOMParser() - -/** - * @summary Try to parse SVG contents and return it data encoded - * - * @param {String} contents - SVG XML contents - * @returns {String|null} - * - * @example - * const encodedSVG = tryParseSVGContents('') - * - * img.src = encodedSVG - */ -const tryParseSVGContents = (contents) => { - const doc = domParser.parseFromString(contents, 'image/svg+xml') - const parserError = doc.querySelector('parsererror') - const svg = doc.querySelector('svg') - - if (!parserError && svg) { - return `data:image/svg+xml,${encodeURIComponent(svg.outerHTML)}` - } - - return null -} - -/** - * @summary SVG element that takes both filepaths and file contents - * @type {Object} - * @public - */ -class SVGIcon extends react.Component { - /** - * @summary Render the SVG - * @returns {react.Element} - */ - render () { - // __dirname behaves strangely inside a Webpack bundle, - // so we need to provide different base directories - // depending on whether __dirname is absolute or not, - // which helps detecting a Webpack bundle. - // We use global.__dirname inside a Webpack bundle since - // that's the only way to get the "real" __dirname. - const baseDirectory = path.isAbsolute(__dirname) - ? path.join(__dirname, '..') - // eslint-disable-next-line no-underscore-dangle - : global.__dirname - - let svgData = '' - - _.find(this.props.contents, (content) => { - const attempt = tryParseSVGContents(content) - - if (attempt) { - svgData = attempt - return true - } - - return false - }) - - if (!svgData) { - _.find(this.props.paths, (relativePath) => { - // 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(baseDirectory, 'assets', relativePath) - - const contents = _.attempt(() => { - return fs.readFileSync(imagePath, { - encoding: 'utf8' - }) - }) - - if (_.isError(contents)) { - analytics.logException(contents) - return false - } - - const parsed = _.attempt(tryParseSVGContents, contents) - - if (parsed) { - svgData = parsed - return true - } - - return false - }) - } - - const width = this.props.width || DEFAULT_SIZE - const height = this.props.height || DEFAULT_SIZE - - return react.createElement('img', { - className: 'svg-icon', - style: { - width, - height - }, - src: svgData, - disabled: this.props.disabled - }) - } - - /** - * @summary Cause a re-render due to changed element properties - * @param {Object} nextProps - the new properties - */ - componentWillReceiveProps (nextProps) { - // This will update the element if the properties change - this.setState(nextProps) - } -} - -SVGIcon.propTypes = { - - /** - * @summary Paths to SVG files to be tried in succession if any fails - */ - paths: propTypes.array, - - /** - * @summary List of embedded SVG contents to be tried in succession if any fails - */ - contents: propTypes.array, - - /** - * @summary SVG image width unit - */ - width: propTypes.string, - - /** - * @summary SVG image height unit - */ - height: propTypes.string, - - /** - * @summary Should the element visually appear grayed out and disabled? - */ - disabled: propTypes.bool - -} - -angularSVGIcon.component('svgIcon', react2angular(SVGIcon)) +angularSVGIcon.component('svgIcon', react2angular(require('./svg-icon/svg-icon.jsx'))) module.exports = MODULE_NAME diff --git a/lib/gui/app/components/svg-icon/svg-icon.jsx b/lib/gui/app/components/svg-icon/svg-icon.jsx new file mode 100644 index 00000000..38f345e8 --- /dev/null +++ b/lib/gui/app/components/svg-icon/svg-icon.jsx @@ -0,0 +1,176 @@ +/* + * Copyright 2018 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.Components.SVGIcon + */ + +const _ = require('lodash') +const react = require('react') +const propTypes = require('prop-types') +const path = require('path') +const fs = require('fs') +const analytics = require('../../modules/analytics') +const domParser = new window.DOMParser() + +const DEFAULT_SIZE = '40px' + +/** + * @summary Try to parse SVG contents and return it data encoded + * + * @param {String} contents - SVG XML contents + * @returns {String|null} + * + * @example + * const encodedSVG = tryParseSVGContents('') + * + * img.src = encodedSVG + */ +const tryParseSVGContents = (contents) => { + const doc = domParser.parseFromString(contents, 'image/svg+xml') + const parserError = doc.querySelector('parsererror') + const svg = doc.querySelector('svg') + + if (!parserError && svg) { + return `data:image/svg+xml,${encodeURIComponent(svg.outerHTML)}` + } + + return null +} + +/* eslint-disable jsdoc/require-example */ + +/** + * @summary SVG element that takes both filepaths and file contents + * @type {Object} + * @public + */ +class SVGIcon extends react.Component { + /** + * @summary Render the SVG + * @returns {react.Element} + */ + render () { + // __dirname behaves strangely inside a Webpack bundle, + // so we need to provide different base directories + // depending on whether __dirname is absolute or not, + // which helps detecting a Webpack bundle. + // We use global.__dirname inside a Webpack bundle since + // that's the only way to get the "real" __dirname. + const baseDirectory = path.isAbsolute(__dirname) + ? path.join(__dirname, '..') + // eslint-disable-next-line no-underscore-dangle + : global.__dirname + + let svgData = '' + + _.find(this.props.contents, (content) => { + const attempt = tryParseSVGContents(content) + + if (attempt) { + svgData = attempt + return true + } + + return false + }) + + if (!svgData) { + _.find(this.props.paths, (relativePath) => { + // 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(baseDirectory, 'assets', relativePath) + + const contents = _.attempt(() => { + return fs.readFileSync(imagePath, { + encoding: 'utf8' + }) + }) + + if (_.isError(contents)) { + analytics.logException(contents) + return false + } + + const parsed = _.attempt(tryParseSVGContents, contents) + + if (parsed) { + svgData = parsed + return true + } + + return false + }) + } + + const width = this.props.width || DEFAULT_SIZE + const height = this.props.height || DEFAULT_SIZE + + return react.createElement('img', { + className: 'svg-icon', + style: { + width, + height + }, + src: svgData, + disabled: this.props.disabled + }) + } + + /** + * @summary Cause a re-render due to changed element properties + * @param {Object} nextProps - the new properties + */ + componentWillReceiveProps (nextProps) { + // This will update the element if the properties change + this.setState(nextProps) + } +} + +SVGIcon.propTypes = { + + /** + * @summary Paths to SVG files to be tried in succession if any fails + */ + paths: propTypes.array, + + /** + * @summary List of embedded SVG contents to be tried in succession if any fails + */ + contents: propTypes.array, + + /** + * @summary SVG image width unit + */ + width: propTypes.string, + + /** + * @summary SVG image height unit + */ + height: propTypes.string, + + /** + * @summary Should the element visually appear grayed out and disabled? + */ + disabled: propTypes.bool + +} + +module.exports = SVGIcon diff --git a/lib/gui/app/models/store.js b/lib/gui/app/models/store.js index 147cd486..2c7123b5 100644 --- a/lib/gui/app/models/store.js +++ b/lib/gui/app/models/store.js @@ -115,7 +115,8 @@ const ACTIONS = _.fromPairs(_.map([ 'DESELECT_DRIVE', 'DESELECT_IMAGE', 'SET_APPLICATION_SESSION_UUID', - 'SET_FLASHING_WORKFLOW_UUID' + 'SET_FLASHING_WORKFLOW_UUID', + 'SET_WEBVIEW_SHOWING_STATUS' ], (message) => { return [ message, message ] })) @@ -511,6 +512,10 @@ const storeReducer = (state = DEFAULT_STATE, action) => { return state.set('flashingWorkflowUuid', action.data) } + case ACTIONS.SET_WEBVIEW_SHOWING_STATUS: { + return state.set('isWebviewShowing', action.data) + } + default: { return state } diff --git a/lib/gui/app/pages/main/controllers/main.js b/lib/gui/app/pages/main/controllers/main.js index c1b1abfb..60d52d0a 100644 --- a/lib/gui/app/pages/main/controllers/main.js +++ b/lib/gui/app/pages/main/controllers/main.js @@ -16,6 +16,7 @@ 'use strict' +const path = require('path') const store = require('../../../models/store') const settings = require('../../../models/settings') const flashState = require('../../../models/flash-state') @@ -25,10 +26,12 @@ const availableDrives = require('../../../models/available-drives') const selectionState = require('../../../models/selection-state') const driveConstraints = require('../../../../../shared/drive-constraints') const messages = require('../../../../../shared/messages') +const prettyBytes = require('pretty-bytes') module.exports = function ( TooltipModalService, - OSOpenExternalService + OSOpenExternalService, + $filter ) { // Expose several modules to the template for convenience this.selection = selectionState @@ -38,6 +41,7 @@ module.exports = function ( this.external = OSOpenExternalService this.constraints = driveConstraints this.progressMessage = messages.progress + this.isWebviewShowing = Boolean(store.getState().toJS().isWebviewShowing) /** * @summary Determine if the drive step should be disabled @@ -93,4 +97,87 @@ module.exports = function ( message: selectionState.getImagePath() }).catch(exceptionReporter.report) } + + /** + * @summary Get drive title based on device quantity + * @function + * @public + * + * @returns {String} - drives title + * + * @example + * console.log(DriveSelectionController.getDrivesTitle()) + * > 'Multiple Drives (4)' + */ + this.getDrivesTitle = () => { + const drives = this.selection.getSelectedDrives() + + /* eslint-disable no-magic-numbers */ + if (drives.length === 1) { + return drives[0].description || 'Untitled Device' + } + /* eslint-enable no-magic-numbers */ + + // eslint-disable-next-line no-magic-numbers + if (drives.length === 0) { + return 'No targets found' + } + + return `${drives.length} Targets` + } + + /** + * @summary Get drive subtitle + * @function + * @public + * + * @returns {String} - drives subtitle + * + * @example + * console.log(MainController.getDrivesSubtitle()) + * > '32 GB' + */ + this.getDrivesSubtitle = () => { + const drive = this.selection.getCurrentDrive() + + if (drive) { + return prettyBytes(drive.size) + } + + return 'Please insert at least one target device' + } + + /** + * @summary Get the basename of the selected image + * @function + * @public + * + * @returns {String} basename of the selected image + * + * @example + * const imageBasename = ImageSelectionController.getImageBasename(); + */ + this.getImageBasename = () => { + if (!this.selection.hasImage()) { + return '' + } + + return path.basename(this.selection.getImagePath()) + } + + this.setWebviewShowing = (data) => { + this.isWebviewShowing = data + store.dispatch({ + type: 'SET_WEBVIEW_SHOWING_STATUS', + data: Boolean(data) + }) + } + + this.getDriveTitle = () => { + /* eslint-disable no-magic-numbers */ + const driveTitleRaw = (this.selection.getSelectedDevices().length === 1) + ? this.getDrivesSubtitle() + : `${this.selection.getSelectedDevices().length} Targets` + return $filter('middleEllipsis:20')(driveTitleRaw) + } } diff --git a/lib/gui/app/pages/main/main.js b/lib/gui/app/pages/main/main.js index 62d26acf..8d2f07c6 100644 --- a/lib/gui/app/pages/main/main.js +++ b/lib/gui/app/pages/main/main.js @@ -41,6 +41,7 @@ const MainPage = angular.module(MODULE_NAME, [ require('../../components/warning-modal/warning-modal'), require('../../components/file-selector'), require('../../components/featured-project'), + require('../../components/reduced-flashing-infos'), require('../../os/open-external/open-external'), require('../../os/dropzone/dropzone'), diff --git a/lib/gui/app/pages/main/templates/main.tpl.html b/lib/gui/app/pages/main/templates/main.tpl.html index 6325e0ff..f1e386ae 100644 --- a/lib/gui/app/pages/main/templates/main.tpl.html +++ b/lib/gui/app/pages/main/templates/main.tpl.html @@ -26,8 +26,8 @@
-
-
+
+
- - +
+ +
+ +
+ +
diff --git a/lib/gui/app/scss/main.scss b/lib/gui/app/scss/main.scss index adb1116a..04e54ca7 100644 --- a/lib/gui/app/scss/main.scss +++ b/lib/gui/app/scss/main.scss @@ -182,7 +182,7 @@ featured-project { width: 0; } - &.isFlashing webview { + &.fp-visible webview { width: 480px; height: 360px; position: absolute; diff --git a/lib/gui/css/main.css b/lib/gui/css/main.css index 46f5e808..7987dd97 100644 --- a/lib/gui/css/main.css +++ b/lib/gui/css/main.css @@ -9952,7 +9952,7 @@ featured-project webview { height: 0; width: 0; } -featured-project.isFlashing webview { +featured-project.fp-visible webview { width: 480px; height: 360px; position: absolute;