mirror of
https://github.com/balena-io/etcher.git
synced 2025-08-28 12:39:24 +00:00
Compare commits
16 Commits
v1.10.13
...
react-driv
Author | SHA1 | Date | |
---|---|---|---|
![]() |
868a35337c | ||
![]() |
1398ca2931 | ||
![]() |
96c865f14a | ||
![]() |
6dbd425e89 | ||
![]() |
5b2769d0e9 | ||
![]() |
5f38cca60c | ||
![]() |
78cebdb7a4 | ||
![]() |
a6aedab0a0 | ||
![]() |
4aeccbe963 | ||
![]() |
126b3fbb40 | ||
![]() |
9ea8a6134e | ||
![]() |
3706770322 | ||
![]() |
7be07bfe8c | ||
![]() |
791c047fa1 | ||
![]() |
35ad0340b9 | ||
![]() |
0a0be3a13d |
@@ -14,3 +14,6 @@ trim_trailing_whitespace = false
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
[*.ts]
|
||||
indent_style = tab
|
||||
|
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1,4 +1,5 @@
|
||||
# Javascript files must retain LF line-endings (to keep eslint happy)
|
||||
*.ts text eol=lf
|
||||
*.js text eol=lf
|
||||
*.jsx text eol=lf
|
||||
# CSS and SCSS files must retain LF line-endings (to keep ensure-staged-sass.sh happy)
|
||||
|
7
Makefile
7
Makefile
@@ -150,6 +150,9 @@ sass:
|
||||
npm rebuild node-sass
|
||||
node-sass lib/gui/app/scss/main.scss > lib/gui/css/main.css
|
||||
|
||||
lint-ts:
|
||||
resin-lint --typescript lib
|
||||
|
||||
lint-js:
|
||||
eslint --ignore-pattern scripts/resin/**/*.js lib tests scripts bin webpack.config.js
|
||||
|
||||
@@ -169,9 +172,9 @@ lint-spell:
|
||||
--skip *.svg *.gz,*.bz2,*.xz,*.zip,*.img,*.dmg,*.iso,*.rpi-sdcard,*.wic,.DS_Store,*.dtb,*.dtbo,*.dat,*.elf,*.bin,*.foo,xz-without-extension \
|
||||
lib tests docs scripts Makefile *.md LICENSE
|
||||
|
||||
lint: lint-js lint-sass lint-cpp lint-html lint-spell
|
||||
lint: lint-ts lint-js lint-sass lint-cpp lint-html lint-spell
|
||||
|
||||
MOCHA_OPTIONS=--recursive --reporter spec
|
||||
MOCHA_OPTIONS=--recursive --reporter spec --require ts-node/register
|
||||
|
||||
# See https://github.com/electron/spectron/issues/127
|
||||
ETCHER_SPECTRON_ENTRYPOINT ?= $(shell node -e 'console.log(require("electron"))')
|
||||
|
@@ -62,7 +62,7 @@ since fresh eyes could help unveil things that we take for granted, but should
|
||||
be documented instead!
|
||||
|
||||
[lego-blocks]: https://github.com/sindresorhus/ama/issues/10#issuecomment-117766328
|
||||
[exit-codes]: https://github.com/balena-io/etcher/blob/master/lib/shared/exit-codes.js
|
||||
[exit-codes]: https://github.com/balena-io/etcher/blob/master/lib/gui/app/modules/exit-codes.js
|
||||
[gui-dir]: https://github.com/balena-io/etcher/tree/master/lib/gui
|
||||
[electron]: http://electron.atom.io
|
||||
[nodejs]: https://nodejs.org
|
||||
|
@@ -31,11 +31,12 @@ const sdk = require('etcher-sdk')
|
||||
const _ = require('lodash')
|
||||
const uuidV4 = require('uuid/v4')
|
||||
|
||||
const EXIT_CODES = require('../../shared/exit-codes')
|
||||
const messages = require('../../shared/messages')
|
||||
const EXIT_CODES = require('../../gui/app/modules/exit-codes')
|
||||
const messages = require('../../gui/app/modules/messages')
|
||||
const store = require('./models/store')
|
||||
const packageJSON = require('../../../package.json')
|
||||
const flashState = require('./models/flash-state')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('./models/settings')
|
||||
const windowProgress = require('./os/window-progress')
|
||||
const analytics = require('./modules/analytics')
|
||||
@@ -45,6 +46,8 @@ const driveScanner = require('./modules/drive-scanner')
|
||||
const osDialog = require('./os/dialog')
|
||||
const exceptionReporter = require('./modules/exception-reporter')
|
||||
const updateLock = require('./modules/update-lock')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const screensaver = require('./modules/screensaver')
|
||||
|
||||
/* eslint-disable lodash/prefer-lodash-method,lodash/prefer-get */
|
||||
|
||||
@@ -453,6 +456,32 @@ app.controller('HeaderController', function (OSOpenExternalService) {
|
||||
this.shouldShowHelp = () => {
|
||||
return !settings.get('disableExternalLinks')
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Whether to show the sleep button
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*
|
||||
* @example
|
||||
* HeaderController.shouldShowSleep()
|
||||
*/
|
||||
this.shouldShowSleep = () => {
|
||||
return settings.get('showScreensaverDelay')
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Enables the screensaver
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* HeaderController.sleep()
|
||||
*/
|
||||
this.sleep = () => {
|
||||
screensaver.off()
|
||||
}
|
||||
})
|
||||
|
||||
app.controller('StateController', function ($rootScope, $scope) {
|
||||
@@ -506,3 +535,5 @@ angular.element(document).ready(() => {
|
||||
angular.bootstrap(document, [ 'Etcher' ])
|
||||
}).catch(exceptionReporter.report)
|
||||
})
|
||||
|
||||
screensaver.init()
|
||||
|
@@ -19,12 +19,13 @@
|
||||
const angular = require('angular')
|
||||
const _ = require('lodash')
|
||||
const Bluebird = require('bluebird')
|
||||
const constraints = require('../../../../../shared/drive-constraints')
|
||||
const constraints = require('../../../../../gui/app/modules/drive-constraints')
|
||||
const store = require('../../../models/store')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
const availableDrives = require('../../../models/available-drives')
|
||||
const selectionState = require('../../../models/selection-state')
|
||||
const utils = require('../../../../../shared/utils')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const utils = require('../../../../../gui/app/modules/utils')
|
||||
|
||||
module.exports = function (
|
||||
$q,
|
||||
|
@@ -14,21 +14,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @module Etcher.Components.TargetSelector
|
||||
*/
|
||||
|
||||
const angular = require('angular')
|
||||
const { react2angular } = require('react2angular')
|
||||
import * as angular from 'angular';
|
||||
import { react2angular } from 'react2angular';
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.TargetSelector'
|
||||
const SelectTargetButton = angular.module(MODULE_NAME, [])
|
||||
const MODULE_NAME = 'Etcher.Components.TargetSelector';
|
||||
const SelectTargetButton = angular.module(MODULE_NAME, []);
|
||||
|
||||
SelectTargetButton.component(
|
||||
'targetSelector',
|
||||
react2angular(require('./target-selector.jsx'))
|
||||
)
|
||||
'targetSelector',
|
||||
react2angular(require('./target-selector.jsx')),
|
||||
);
|
||||
|
||||
module.exports = MODULE_NAME
|
||||
export = MODULE_NAME;
|
@@ -31,7 +31,7 @@ const {
|
||||
} = require('./../../styled-components')
|
||||
const { Txt } = require('rendition')
|
||||
const middleEllipsis = require('./../../utils/middle-ellipsis')
|
||||
const { bytesToClosestUnit } = require('./../../../../shared/units')
|
||||
const { bytesToClosestUnit } = require('./../../../../gui/app/modules/units')
|
||||
|
||||
const TargetDetail = styled((props) => (
|
||||
<Txt.span {...props}>
|
||||
|
264
lib/gui/app/components/drive-selector2/drive-selector.tsx
Normal file
264
lib/gui/app/components/drive-selector2/drive-selector.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
* Copyright 2019 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.
|
||||
*/
|
||||
|
||||
import { Meter } from 'grommet';
|
||||
import { sortBy } from 'lodash';
|
||||
import * as React from 'react';
|
||||
import { Badge, Modal, Table } from 'rendition';
|
||||
|
||||
import { getDrives } from '../../models/available-drives';
|
||||
import { COMPATIBILITY_STATUS_TYPES } from '../../modules/drive-constraints';
|
||||
import { subscribe } from '../../models/store';
|
||||
import { ThemedProvider } from '../../styled-components';
|
||||
import { bytesToClosestUnit } from '../../modules/units';
|
||||
|
||||
interface Drive {
|
||||
description: string;
|
||||
device: string;
|
||||
isSystem: boolean;
|
||||
isReadOnly: boolean;
|
||||
progress?: number;
|
||||
size?: number;
|
||||
link?: string;
|
||||
linkCTA?: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
interface CompatibilityStatus {
|
||||
type: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface DriveSelectorProps {
|
||||
title: string;
|
||||
close: () => void;
|
||||
setSelectedDrives: (drives: Drive[]) => void;
|
||||
isDriveSelected: (drive: Drive) => boolean;
|
||||
isDriveValid: (drive: Drive) => boolean;
|
||||
getDriveBadges: (drive: Drive) => CompatibilityStatus[];
|
||||
}
|
||||
|
||||
interface DriveSelectorState {
|
||||
drives: Drive[];
|
||||
selected: Drive[];
|
||||
disabledDrives: string[];
|
||||
}
|
||||
|
||||
const modalStyle = {
|
||||
width: '800px',
|
||||
height: '600px',
|
||||
paddingTop: '20px',
|
||||
paddingLeft: '30px',
|
||||
paddingRight: '30px',
|
||||
paddingBottom: '11px',
|
||||
};
|
||||
|
||||
const titleStyle = {
|
||||
color: '#2a506f',
|
||||
};
|
||||
|
||||
const subtitleStyle = {
|
||||
marginLeft: '10px',
|
||||
fontSize: '11px',
|
||||
color: '#5b82a7',
|
||||
};
|
||||
|
||||
const wrapperStyle = {
|
||||
height: '250px',
|
||||
overflowX: 'hidden' as 'hidden',
|
||||
overflowY: 'auto' as 'auto',
|
||||
};
|
||||
|
||||
export class DriveSelector2 extends React.Component<
|
||||
DriveSelectorProps,
|
||||
DriveSelectorState
|
||||
> {
|
||||
private table: Table<Drive> | null = null;
|
||||
private columns: {
|
||||
field: keyof Drive;
|
||||
label: string;
|
||||
render?: (value: any, row: Drive) => string | number | JSX.Element | null;
|
||||
}[];
|
||||
private unsubscribe?: () => void;
|
||||
|
||||
constructor(props: DriveSelectorProps) {
|
||||
super(props);
|
||||
this.columns = [
|
||||
{
|
||||
field: 'description',
|
||||
label: 'Name',
|
||||
} as const,
|
||||
{
|
||||
field: 'size',
|
||||
label: 'Size',
|
||||
render: this.renderSize.bind(this),
|
||||
} as const,
|
||||
{
|
||||
field: 'displayName',
|
||||
label: 'Location',
|
||||
render: this.renderLocation.bind(this),
|
||||
} as const,
|
||||
{
|
||||
field: 'isReadOnly', // We don't use this, but a valid field that is not used in another column is required
|
||||
label: ' ',
|
||||
render: this.renderBadges.bind(this),
|
||||
} as const,
|
||||
];
|
||||
this.state = this.getNewState();
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.update();
|
||||
if (this.unsubscribe === undefined) {
|
||||
this.unsubscribe = subscribe(this.update.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.unsubscribe !== undefined) {
|
||||
this.unsubscribe();
|
||||
this.unsubscribe = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private getNewState() {
|
||||
let drives: Drive[] = getDrives();
|
||||
for (let i = 0; i < drives.length; i++) {
|
||||
drives[i] = { ...drives[i] };
|
||||
}
|
||||
drives = sortBy(drives, 'device');
|
||||
const selected = drives.filter(d => this.props.isDriveSelected(d));
|
||||
const disabledDrives = drives
|
||||
.filter(d => !this.props.isDriveValid(d))
|
||||
.map(d => d.device);
|
||||
return { drives, disabledDrives, selected };
|
||||
}
|
||||
|
||||
private update() {
|
||||
this.setState(this.getNewState());
|
||||
this.updateTableSelection();
|
||||
}
|
||||
|
||||
private updateTableSelection() {
|
||||
if (this.table !== null) {
|
||||
this.table.setRowSelection(this.state.selected);
|
||||
}
|
||||
}
|
||||
|
||||
private renderSize(size: number) {
|
||||
if (size) {
|
||||
return bytesToClosestUnit(size);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private renderLocation(displayName: string, drive: Drive) {
|
||||
const result: Array<string | JSX.Element> = [displayName];
|
||||
if (drive.link && drive.linkCTA) {
|
||||
result.push(<a href={drive.link}>{drive.linkCTA}</a>);
|
||||
}
|
||||
return <React.Fragment>{result}</React.Fragment>;
|
||||
}
|
||||
|
||||
private renderBadges(_value: any, row: Drive) {
|
||||
const result = [];
|
||||
if (row.progress !== undefined) {
|
||||
result.push(
|
||||
<Meter
|
||||
size="small"
|
||||
thickness="xxsmall"
|
||||
values={[
|
||||
{
|
||||
value: row.progress,
|
||||
label: row.progress + '%',
|
||||
color: '#2297de',
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
result.push(
|
||||
...this.props.getDriveBadges(row).map((status: CompatibilityStatus) => {
|
||||
const props: {
|
||||
key: string;
|
||||
xsmall: true;
|
||||
danger?: boolean;
|
||||
warning?: boolean;
|
||||
} = { xsmall: true, key: status.message };
|
||||
if (status.type === COMPATIBILITY_STATUS_TYPES.ERROR) {
|
||||
props.danger = true;
|
||||
} else if (status.type === COMPATIBILITY_STATUS_TYPES.WARNING) {
|
||||
props.warning = true;
|
||||
}
|
||||
return <Badge {...props}>{status.message}</Badge>;
|
||||
}),
|
||||
);
|
||||
return <React.Fragment>{result}</React.Fragment>;
|
||||
}
|
||||
|
||||
private renderTbodyPrefix() {
|
||||
if (this.state.drives.length === 0) {
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={this.columns.length} style={{ textAlign: 'center' }}>
|
||||
<b>Connect a drive</b>
|
||||
<div>No removable drive detected.</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<Modal
|
||||
titleElement={
|
||||
<div style={titleStyle}>
|
||||
{this.props.title}
|
||||
<span style={subtitleStyle}>
|
||||
{this.state.drives.length} found
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
action={`Select (${this.state.selected.length})`}
|
||||
style={modalStyle}
|
||||
done={this.props.close}
|
||||
>
|
||||
<div style={wrapperStyle}>
|
||||
<Table<Drive>
|
||||
ref={t => {
|
||||
this.table = t;
|
||||
this.updateTableSelection();
|
||||
}}
|
||||
rowKey="device"
|
||||
onCheck={this.onCheck.bind(this)}
|
||||
columns={this.columns}
|
||||
data={this.state.drives}
|
||||
disabledRows={this.state.disabledDrives}
|
||||
tbodyPrefix={this.renderTbodyPrefix()}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</ThemedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
private onCheck(checkedDrives: Drive[]): void {
|
||||
this.props.setSelectedDrives(checkedDrives);
|
||||
}
|
||||
}
|
38
lib/gui/app/components/drive-selector2/index.ts
Normal file
38
lib/gui/app/components/drive-selector2/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2019 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.
|
||||
*/
|
||||
|
||||
import * as angular from 'angular';
|
||||
import { react2angular } from 'react2angular';
|
||||
|
||||
import { DriveSelector2 } from './drive-selector.tsx';
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.DriveSelector2';
|
||||
|
||||
angular
|
||||
.module(MODULE_NAME, [])
|
||||
.component(
|
||||
'driveSelector2',
|
||||
react2angular(DriveSelector2, [
|
||||
'close',
|
||||
'getDriveBadges',
|
||||
'isDriveSelected',
|
||||
'isDriveValid',
|
||||
'setSelectedDrives',
|
||||
'title',
|
||||
]),
|
||||
);
|
||||
|
||||
export = MODULE_NAME;
|
@@ -18,8 +18,10 @@
|
||||
|
||||
const _ = require('lodash')
|
||||
const os = require('os')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../../../models/settings')
|
||||
const utils = require('../../../../../shared/utils')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const utils = require('../../../../../gui/app/modules/utils')
|
||||
const angular = require('angular')
|
||||
|
||||
/* eslint-disable lodash/prefer-lodash-method */
|
||||
|
@@ -25,7 +25,7 @@ const colors = require('./colors')
|
||||
const prettyBytes = require('pretty-bytes')
|
||||
const files = require('../../../models/files')
|
||||
const middleEllipsis = require('../../../utils/middle-ellipsis')
|
||||
const supportedFormats = require('../../../../../shared/supported-formats')
|
||||
const supportedFormats = require('../../../../../gui/app/modules/supported-formats')
|
||||
|
||||
const debug = require('debug')('etcher:gui:file-selector')
|
||||
|
||||
|
@@ -35,9 +35,9 @@ const selectionState = require('../../../models/selection-state')
|
||||
const store = require('../../../models/store')
|
||||
const osDialog = require('../../../os/dialog')
|
||||
const exceptionReporter = require('../../../modules/exception-reporter')
|
||||
const messages = require('../../../../../shared/messages')
|
||||
const errors = require('../../../../../shared/errors')
|
||||
const supportedFormats = require('../../../../../shared/supported-formats')
|
||||
const messages = require('../../../../../gui/app/modules/messages')
|
||||
const errors = require('../../../../../gui/app/modules/errors')
|
||||
const supportedFormats = require('../../../../../gui/app/modules/supported-formats')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
|
||||
const debug = require('debug')('etcher:gui:file-selector')
|
||||
|
@@ -19,10 +19,12 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
const React = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const { Badge, DropDownButton, Select } = require('rendition')
|
||||
const { default: styled } = require('styled-components')
|
||||
|
||||
const middleEllipsis = require('./../../utils/middle-ellipsis')
|
||||
|
||||
const shared = require('./../../../../shared/units')
|
||||
const shared = require('./../../../../gui/app/modules/units')
|
||||
const {
|
||||
StepButton,
|
||||
StepNameButton,
|
||||
@@ -34,6 +36,17 @@ const {
|
||||
ThemedProvider
|
||||
} = require('./../../styled-components')
|
||||
|
||||
const DropdownItem = styled.p`
|
||||
padding-top: 10px;
|
||||
text-align: left;
|
||||
width: 150px;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const DropdownItemIcon = styled.i`
|
||||
padding-right: 10px;
|
||||
`
|
||||
|
||||
const SelectImageButton = (props) => {
|
||||
if (props.hasImage) {
|
||||
return (
|
||||
@@ -50,9 +63,9 @@ const SelectImageButton = (props) => {
|
||||
<ChangeButton
|
||||
plain
|
||||
mb={14}
|
||||
onClick={props.reselectImage}
|
||||
onClick={props.deselectImage}
|
||||
>
|
||||
Change
|
||||
Remove
|
||||
</ChangeButton>
|
||||
}
|
||||
<DetailsText>
|
||||
@@ -64,11 +77,26 @@ const SelectImageButton = (props) => {
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<StepSelection>
|
||||
<StepButton
|
||||
onClick={props.openImageSelector}
|
||||
<DropDownButton
|
||||
primary
|
||||
label={
|
||||
<div onClick={props.openImageSelector}>Select image</div>
|
||||
}
|
||||
style={{height: '48px'}}
|
||||
>
|
||||
Select image
|
||||
</StepButton>
|
||||
<DropdownItem
|
||||
onClick={props.openImageSelector}
|
||||
>
|
||||
<DropdownItemIcon className="far fa-file"/>
|
||||
Select image file
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
onClick={props.openDriveSelector}
|
||||
>
|
||||
<DropdownItemIcon className="far fa-copy"/>
|
||||
Duplicate drive
|
||||
</DropdownItem>
|
||||
</DropDownButton>
|
||||
<Footer>
|
||||
{ props.mainSupportedExtensions.join(', ') }, and{' '}
|
||||
<Underline
|
||||
@@ -84,15 +112,17 @@ const SelectImageButton = (props) => {
|
||||
|
||||
SelectImageButton.propTypes = {
|
||||
openImageSelector: propTypes.func,
|
||||
openDriveSelector: propTypes.func,
|
||||
mainSupportedExtensions: propTypes.array,
|
||||
extraSupportedExtensions: propTypes.array,
|
||||
hasImage: propTypes.bool,
|
||||
showSelectedImageDetails: propTypes.func,
|
||||
imageName: propTypes.string,
|
||||
imageBasename: propTypes.string,
|
||||
reselectImage: propTypes.func,
|
||||
deselectImage: propTypes.func,
|
||||
flashing: propTypes.bool,
|
||||
imageSize: propTypes.number
|
||||
imageSize: propTypes.number,
|
||||
sourceType: propTypes.string
|
||||
}
|
||||
|
||||
module.exports = SelectImageButton
|
||||
|
@@ -11,22 +11,36 @@
|
||||
</head>
|
||||
<body>
|
||||
<header class="section-header" ng-controller="HeaderController as header">
|
||||
<button
|
||||
class="button button-link sleep-button"
|
||||
tabindex="4"
|
||||
ng-if="header.shouldShowSleep()"
|
||||
ng-click="header.sleep()"
|
||||
>
|
||||
<svg-icon paths="[ '../../assets/moon.svg' ]"
|
||||
width="'14px'"
|
||||
height="'14px'"></svg-icon>
|
||||
<span >
|
||||
Sleep
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button class="button button-link"
|
||||
ng-if="header.shouldShowHelp()"
|
||||
ng-click="header.openHelpPage()"
|
||||
tabindex="4">
|
||||
tabindex="5">
|
||||
<span class="glyphicon glyphicon-question-sign"></span>
|
||||
</button>
|
||||
|
||||
<button class="button button-link"
|
||||
ui-sref="settings"
|
||||
hide-if-state="settings"
|
||||
tabindex="5">
|
||||
tabindex="6">
|
||||
<span class="glyphicon glyphicon-cog"></span>
|
||||
</button>
|
||||
|
||||
<button class="button button-link"
|
||||
tabindex="5"
|
||||
tabindex="7"
|
||||
ui-sref="main"
|
||||
show-if-state="settings">
|
||||
<span class="glyphicon glyphicon-chevron-left"></span> Back
|
||||
|
@@ -18,7 +18,7 @@
|
||||
|
||||
const _ = require('lodash')
|
||||
const store = require('./store')
|
||||
const units = require('../../../shared/units')
|
||||
const units = require('../../../gui/app/modules/units')
|
||||
|
||||
/**
|
||||
* @summary Reset flash state
|
||||
|
70
lib/gui/app/models/leds.ts
Normal file
70
lib/gui/app/models/leds.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { delay } from 'bluebird';
|
||||
import { Gpio } from 'pigpio';
|
||||
|
||||
class Led {
|
||||
private gpio: Gpio;
|
||||
|
||||
constructor(gpioNumber: number) {
|
||||
this.gpio = new Gpio(gpioNumber, { mode: Gpio.OUTPUT });
|
||||
}
|
||||
|
||||
public set intensity(value: number) {
|
||||
// TODO: check that 0 <= value <= 1
|
||||
this.gpio.pwmWrite(Math.round(value * 255));
|
||||
}
|
||||
}
|
||||
|
||||
export type Color = [number, number, number];
|
||||
export type AnimationFunction = (t: number) => Color;
|
||||
|
||||
export class RGBLed {
|
||||
private leds: [Led, Led, Led];
|
||||
private currentAnimation?: AnimationFunction;
|
||||
private static animations: Map<string, AnimationFunction> = new Map();
|
||||
|
||||
constructor(gpioNumbers: [number, number, number], public frequency = 60) {
|
||||
this.leds = gpioNumbers.map(n => new Led(n)) as [Led, Led, Led];
|
||||
}
|
||||
|
||||
private async loop() {
|
||||
while (this.currentAnimation !== undefined) {
|
||||
this.$setColor(...this.currentAnimation(new Date().getTime()));
|
||||
await delay(1000 / this.frequency);
|
||||
}
|
||||
}
|
||||
|
||||
private $setColor(red: number, green: number, blue: number) {
|
||||
this.leds[0].intensity = red;
|
||||
this.leds[1].intensity = green;
|
||||
this.leds[2].intensity = blue;
|
||||
}
|
||||
|
||||
public setColor(red: number, green: number, blue: number) {
|
||||
// stop any running animation
|
||||
this.setAnimation();
|
||||
this.$setColor(red, green, blue);
|
||||
}
|
||||
|
||||
public static registerAnimation(name: string, animation: AnimationFunction) {
|
||||
RGBLed.animations.set(name, animation);
|
||||
}
|
||||
|
||||
public setAnimation(name?: string) {
|
||||
const hadAnimation = this.currentAnimation !== undefined;
|
||||
this.currentAnimation = name ? RGBLed.animations.get(name) : undefined;
|
||||
// Don't launch the loop a second time
|
||||
if (!hadAnimation) {
|
||||
this.loop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RGBLed.registerAnimation('breathe-white', (t: number) => {
|
||||
const intensity = Math.sin(t / 1000);
|
||||
return [intensity, intensity, intensity];
|
||||
});
|
||||
|
||||
RGBLed.registerAnimation('blink-white', (t: number) => {
|
||||
const intensity = Math.floor(t / 1000) % 2;
|
||||
return [intensity, intensity, intensity];
|
||||
});
|
@@ -14,18 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
import { app, remote } from 'electron';
|
||||
import { readFile, unlink, writeFile } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { inspect, promisify } from 'util';
|
||||
|
||||
const Bluebird = require('bluebird')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const readFileAsync = promisify(readFile);
|
||||
const writeFileAsync = promisify(writeFile);
|
||||
const unlinkAsync = promisify(unlink);
|
||||
|
||||
/**
|
||||
* @summary Number of spaces to indent JSON output with
|
||||
* @type {Number}
|
||||
* @constant
|
||||
*/
|
||||
const JSON_INDENT = 2
|
||||
const JSON_INDENT = 2;
|
||||
|
||||
/**
|
||||
* @summary Userdata directory path
|
||||
@@ -38,21 +41,16 @@ const JSON_INDENT = 2
|
||||
* @constant
|
||||
* @type {String}
|
||||
*/
|
||||
const USER_DATA_DIR = (() => {
|
||||
// NOTE: The ternary is due to this module being loaded both,
|
||||
// Electron's main process and renderer process
|
||||
const electron = require('electron')
|
||||
return electron.app
|
||||
? electron.app.getPath('userData')
|
||||
: electron.remote.app.getPath('userData')
|
||||
})()
|
||||
// NOTE: The ternary is due to this module being loaded both,
|
||||
// Electron's main process and renderer process
|
||||
const USER_DATA_DIR = (app || remote.app).getPath('userData');
|
||||
|
||||
/**
|
||||
* @summary Configuration file path
|
||||
* @type {String}
|
||||
* @constant
|
||||
*/
|
||||
const CONFIG_PATH = path.join(USER_DATA_DIR, 'config.json')
|
||||
const CONFIG_PATH = join(USER_DATA_DIR, 'config.json');
|
||||
|
||||
/**
|
||||
* @summary Read a local config.json file
|
||||
@@ -68,26 +66,15 @@ const CONFIG_PATH = path.join(USER_DATA_DIR, 'config.json')
|
||||
* console.log(settings)
|
||||
* })
|
||||
*/
|
||||
const readConfigFile = (filename) => {
|
||||
return new Bluebird((resolve, reject) => {
|
||||
fs.readFile(filename, { encoding: 'utf8' }, (error, contents) => {
|
||||
let data = {}
|
||||
if (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
resolve(data)
|
||||
} else {
|
||||
reject(error)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
data = JSON.parse(contents)
|
||||
} catch (parseError) {
|
||||
console.error(parseError)
|
||||
}
|
||||
resolve(data)
|
||||
}
|
||||
})
|
||||
})
|
||||
async function readConfigFile(filename: string): Promise<any> {
|
||||
try {
|
||||
return JSON.parse(await readFileAsync(filename, { encoding: 'utf8' }));
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to load settings from ${filename}: ${inspect(error)}`,
|
||||
);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,17 +93,10 @@ const readConfigFile = (filename) => {
|
||||
* console.log('data written')
|
||||
* })
|
||||
*/
|
||||
const writeConfigFile = (filename, data) => {
|
||||
return new Bluebird((resolve, reject) => {
|
||||
const contents = JSON.stringify(data, null, JSON_INDENT)
|
||||
fs.writeFile(filename, contents, (error) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(data)
|
||||
}
|
||||
})
|
||||
})
|
||||
async function writeConfigFile(filename: string, data: any) {
|
||||
const contents = JSON.stringify(data, null, JSON_INDENT);
|
||||
await writeFileAsync(filename, contents);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,8 +112,8 @@ const writeConfigFile = (filename, data) => {
|
||||
* console.log(settings);
|
||||
* });
|
||||
*/
|
||||
exports.readAll = () => {
|
||||
return readConfigFile(CONFIG_PATH)
|
||||
export async function readAll(): Promise<any> {
|
||||
return await readConfigFile(CONFIG_PATH);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,8 +132,8 @@ exports.readAll = () => {
|
||||
* console.log('Done!');
|
||||
* });
|
||||
*/
|
||||
exports.writeAll = (settings) => {
|
||||
return writeConfigFile(CONFIG_PATH, settings)
|
||||
export async function writeAll(settings: any) {
|
||||
return await writeConfigFile(CONFIG_PATH, settings);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,14 +151,6 @@ exports.writeAll = (settings) => {
|
||||
* console.log('Done!');
|
||||
* });
|
||||
*/
|
||||
exports.clear = () => {
|
||||
return new Bluebird((resolve, reject) => {
|
||||
fs.unlink(CONFIG_PATH, (error) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
export async function clear(): Promise<void> {
|
||||
await unlinkAsync(CONFIG_PATH);
|
||||
}
|
@@ -1,232 +0,0 @@
|
||||
/*
|
||||
* 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.Models.Settings
|
||||
*/
|
||||
|
||||
const _ = require('lodash')
|
||||
const Bluebird = require('bluebird')
|
||||
const localSettings = require('./local-settings')
|
||||
const errors = require('../../../shared/errors')
|
||||
const packageJSON = require('../../../../package.json')
|
||||
const debug = require('debug')('etcher:models:settings')
|
||||
|
||||
/**
|
||||
* @summary Default settings
|
||||
* @constant
|
||||
* @type {Object}
|
||||
*/
|
||||
const DEFAULT_SETTINGS = {
|
||||
unsafeMode: false,
|
||||
errorReporting: true,
|
||||
unmountOnSuccess: true,
|
||||
validateWriteOnSuccess: true,
|
||||
trim: false,
|
||||
updatesEnabled: packageJSON.updates.enabled && !_.includes([ 'rpm', 'deb' ], packageJSON.packageType),
|
||||
lastSleptUpdateNotifier: null,
|
||||
lastSleptUpdateNotifierVersion: null,
|
||||
desktopNotifications: true
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Settings state
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
let settings = _.cloneDeep(DEFAULT_SETTINGS)
|
||||
|
||||
/**
|
||||
* @summary Reset settings to their default values
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* settings.reset().then(() => {
|
||||
* console.log('Done!');
|
||||
* });
|
||||
*/
|
||||
exports.reset = () => {
|
||||
debug('reset')
|
||||
|
||||
// TODO: Remove default settings from config file (?)
|
||||
settings = _.cloneDeep(DEFAULT_SETTINGS)
|
||||
return localSettings.writeAll(settings)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Extend the current settings
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Object} value - value
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* settings.assign({
|
||||
* foo: 'bar'
|
||||
* }).then(() => {
|
||||
* console.log('Done!');
|
||||
* });
|
||||
*/
|
||||
exports.assign = (value) => {
|
||||
debug('assign', value)
|
||||
if (_.isNil(value)) {
|
||||
return Bluebird.reject(errors.createError({
|
||||
title: 'Missing settings'
|
||||
}))
|
||||
}
|
||||
|
||||
if (!_.isPlainObject(value)) {
|
||||
return Bluebird.reject(errors.createError({
|
||||
title: 'Settings must be an object'
|
||||
}))
|
||||
}
|
||||
|
||||
const newSettings = _.assign({}, settings, value)
|
||||
|
||||
return localSettings.writeAll(newSettings)
|
||||
.then((updatedSettings) => {
|
||||
// NOTE: Only update in memory settings when successfully written
|
||||
settings = updatedSettings
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Extend the application state with the local settings
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* settings.load().then(() => {
|
||||
* console.log('Done!');
|
||||
* });
|
||||
*/
|
||||
exports.load = () => {
|
||||
debug('load')
|
||||
return localSettings.readAll().then((loadedSettings) => {
|
||||
return _.assign(settings, loadedSettings)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Set a setting value
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {String} key - setting key
|
||||
* @param {*} value - setting value
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* settings.set('unmountOnSuccess', true).then(() => {
|
||||
* console.log('Done!');
|
||||
* });
|
||||
*/
|
||||
exports.set = (key, value) => {
|
||||
debug('set', key, value)
|
||||
if (_.isNil(key)) {
|
||||
return Bluebird.reject(errors.createError({
|
||||
title: 'Missing setting key'
|
||||
}))
|
||||
}
|
||||
|
||||
if (!_.isString(key)) {
|
||||
return Bluebird.reject(errors.createError({
|
||||
title: `Invalid setting key: ${key}`
|
||||
}))
|
||||
}
|
||||
|
||||
const previousValue = settings[key]
|
||||
|
||||
settings[key] = value
|
||||
|
||||
return localSettings.writeAll(settings)
|
||||
.catch((error) => {
|
||||
// Revert to previous value if persisting settings failed
|
||||
settings[key] = previousValue
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get a setting value
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {String} key - setting key
|
||||
* @returns {*} setting value
|
||||
*
|
||||
* @example
|
||||
* const value = settings.get('unmountOnSuccess');
|
||||
*/
|
||||
exports.get = (key) => {
|
||||
return _.cloneDeep(_.get(settings, [ key ]))
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if setting value exists
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {String} key - setting key
|
||||
* @returns {Boolean} exists
|
||||
*
|
||||
* @example
|
||||
* const hasValue = settings.has('unmountOnSuccess');
|
||||
*/
|
||||
exports.has = (key) => {
|
||||
/* eslint-disable no-eq-null */
|
||||
return settings[key] != null
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get all setting values
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {Object} all setting values
|
||||
*
|
||||
* @example
|
||||
* const allSettings = settings.getAll();
|
||||
* console.log(allSettings.unmountOnSuccess);
|
||||
*/
|
||||
exports.getAll = () => {
|
||||
debug('getAll')
|
||||
return _.cloneDeep(settings)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get the default setting values
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {Object} all setting values
|
||||
*
|
||||
* @example
|
||||
* const defaults = settings.getDefaults();
|
||||
* console.log(defaults.unmountOnSuccess);
|
||||
*/
|
||||
exports.getDefaults = () => {
|
||||
debug('getDefaults')
|
||||
return _.cloneDeep(DEFAULT_SETTINGS)
|
||||
}
|
103
lib/gui/app/models/settings.ts
Normal file
103
lib/gui/app/models/settings.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as debug_ from 'debug';
|
||||
import { EventEmitter } from 'events';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import { createError } from '../modules/errors';
|
||||
import { Dict } from '../modules/utils';
|
||||
import { readAll, writeAll } from './local-settings';
|
||||
|
||||
import * as packageJSON from '../../../../package.json';
|
||||
|
||||
const debug = debug_('etcher:models:settings');
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
unsafeMode: false,
|
||||
errorReporting: true,
|
||||
unmountOnSuccess: true,
|
||||
validateWriteOnSuccess: true,
|
||||
trim: false,
|
||||
updatesEnabled:
|
||||
packageJSON.updates.enabled &&
|
||||
!['rpm', 'deb'].includes(packageJSON.packageType),
|
||||
lastSleptUpdateNotifier: null,
|
||||
lastSleptUpdateNotifierVersion: null,
|
||||
desktopNotifications: true,
|
||||
};
|
||||
|
||||
let settings: Dict<any> = cloneDeep(DEFAULT_SETTINGS);
|
||||
|
||||
export const events = new EventEmitter();
|
||||
|
||||
// Exported for tests only, don't use that
|
||||
export async function reset(): Promise<void> {
|
||||
debug('reset');
|
||||
settings = cloneDeep(DEFAULT_SETTINGS);
|
||||
await writeAll(settings);
|
||||
}
|
||||
|
||||
export async function load(): Promise<any> {
|
||||
debug('load');
|
||||
const loadedSettings = await readAll();
|
||||
const oldSettings = cloneDeep(settings);
|
||||
settings = { ...settings, ...loadedSettings };
|
||||
for (const key of Object.keys(settings)) {
|
||||
const value = settings[key];
|
||||
if (!oldSettings.hasOwnProperty(key) || value !== oldSettings[key]) {
|
||||
events.emit(key, value);
|
||||
}
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
export async function set(key: string, value: any): Promise<void> {
|
||||
debug('set', key, value);
|
||||
if (typeof key !== 'string') {
|
||||
throw createError({ title: `Invalid setting key: ${key}` });
|
||||
}
|
||||
const previousValue = settings[key];
|
||||
settings[key] = value;
|
||||
try {
|
||||
await writeAll(settings);
|
||||
} catch (error) {
|
||||
// Revert to previous value if persisting settings failed
|
||||
settings[key] = previousValue;
|
||||
throw error;
|
||||
}
|
||||
if (value !== previousValue) {
|
||||
events.emit(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
export function get(key: string): any {
|
||||
return cloneDeep(settings[key]);
|
||||
}
|
||||
|
||||
export function has(key: string): boolean {
|
||||
return settings[key] !== undefined;
|
||||
}
|
||||
|
||||
export function getAll(): any {
|
||||
debug('getAll');
|
||||
return cloneDeep(settings);
|
||||
}
|
||||
|
||||
export function getDefaults(): any {
|
||||
debug('getDefaults');
|
||||
return cloneDeep(DEFAULT_SETTINGS);
|
||||
}
|
@@ -20,11 +20,14 @@ const Immutable = require('immutable')
|
||||
const _ = require('lodash')
|
||||
const redux = require('redux')
|
||||
const uuidV4 = require('uuid/v4')
|
||||
const constraints = require('../../../shared/drive-constraints')
|
||||
const supportedFormats = require('../../../shared/supported-formats')
|
||||
const errors = require('../../../shared/errors')
|
||||
const fileExtensions = require('../../../shared/file-extensions')
|
||||
const utils = require('../../../shared/utils')
|
||||
const constraints = require('../modules/drive-constraints')
|
||||
const supportedFormats = require('../modules/supported-formats')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const errors = require('../modules/errors')
|
||||
const fileExtensions = require('../modules/file-extensions')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const utils = require('../modules/utils')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('./settings')
|
||||
|
||||
/**
|
||||
@@ -65,10 +68,7 @@ const flashStateNoNilFields = [
|
||||
* @constant
|
||||
* @private
|
||||
*/
|
||||
const selectImageNoNilFields = [
|
||||
'path',
|
||||
'extension'
|
||||
]
|
||||
const selectImageNoNilFields = [ 'path' ]
|
||||
|
||||
/**
|
||||
* @summary Application default state
|
||||
@@ -382,42 +382,44 @@ const storeReducer = (state = DEFAULT_STATE, action) => {
|
||||
})
|
||||
}
|
||||
|
||||
if (!_.isString(action.data.extension)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image extension: ${action.data.extension}`
|
||||
})
|
||||
}
|
||||
|
||||
const extension = _.toLower(action.data.extension)
|
||||
|
||||
if (!_.includes(supportedFormats.getAllExtensions(), extension)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image extension: ${action.data.extension}`
|
||||
})
|
||||
}
|
||||
|
||||
let lastImageExtension = fileExtensions.getLastFileExtension(action.data.path)
|
||||
lastImageExtension = _.isString(lastImageExtension) ? _.toLower(lastImageExtension) : lastImageExtension
|
||||
|
||||
if (lastImageExtension !== extension) {
|
||||
if (!_.isString(action.data.archiveExtension)) {
|
||||
if (!action.data.isDrive) { // We don't care about extensions if the source is a drive
|
||||
if (!_.isString(action.data.extension)) {
|
||||
throw errors.createError({
|
||||
title: 'Missing image archive extension'
|
||||
title: `Invalid image extension: ${action.data.extension}`
|
||||
})
|
||||
}
|
||||
|
||||
const archiveExtension = _.toLower(action.data.archiveExtension)
|
||||
const extension = _.toLower(action.data.extension)
|
||||
|
||||
if (!_.includes(supportedFormats.getAllExtensions(), archiveExtension)) {
|
||||
if (!_.includes(supportedFormats.getAllExtensions(), extension)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image archive extension: ${action.data.archiveExtension}`
|
||||
title: `Invalid image extension: ${action.data.extension}`
|
||||
})
|
||||
}
|
||||
|
||||
if (lastImageExtension !== archiveExtension) {
|
||||
throw errors.createError({
|
||||
title: `Image archive extension mismatch: ${action.data.archiveExtension} and ${lastImageExtension}`
|
||||
})
|
||||
let lastImageExtension = fileExtensions.getLastFileExtension(action.data.path)
|
||||
lastImageExtension = _.isString(lastImageExtension) ? _.toLower(lastImageExtension) : lastImageExtension
|
||||
|
||||
if (lastImageExtension !== extension) {
|
||||
if (!_.isString(action.data.archiveExtension)) {
|
||||
throw errors.createError({
|
||||
title: 'Missing image archive extension'
|
||||
})
|
||||
}
|
||||
|
||||
const archiveExtension = _.toLower(action.data.archiveExtension)
|
||||
|
||||
if (!_.includes(supportedFormats.getAllExtensions(), archiveExtension)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image archive extension: ${action.data.archiveExtension}`
|
||||
})
|
||||
}
|
||||
|
||||
if (lastImageExtension !== archiveExtension) {
|
||||
throw errors.createError({
|
||||
title: `Image archive extension mismatch: ${action.data.archiveExtension} and ${lastImageExtension}`
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -19,8 +19,10 @@
|
||||
const _ = require('lodash')
|
||||
const resinCorvus = require('resin-corvus/browser')
|
||||
const packageJSON = require('../../../../package.json')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../models/settings')
|
||||
const { getConfig, hasProps } = require('../../../shared/utils')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const { getConfig, hasProps } = require('../../../gui/app/modules/utils')
|
||||
|
||||
const sentryToken = settings.get('analyticsSentryToken') ||
|
||||
_.get(packageJSON, [ 'analytics', 'sentry', 'token' ])
|
||||
|
@@ -20,8 +20,9 @@ const Bluebird = require('bluebird')
|
||||
const _ = require('lodash')
|
||||
const ipc = require('node-ipc')
|
||||
const sdk = require('etcher-sdk')
|
||||
const EXIT_CODES = require('../../shared/exit-codes')
|
||||
const errors = require('../../shared/errors')
|
||||
const EXIT_CODES = require('./exit-codes')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const errors = require('./errors')
|
||||
|
||||
ipc.config.id = process.env.IPC_CLIENT_ID
|
||||
ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT
|
@@ -19,6 +19,7 @@
|
||||
const sdk = require('etcher-sdk')
|
||||
const process = require('process')
|
||||
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../models/settings')
|
||||
|
||||
/**
|
||||
|
347
lib/gui/app/modules/errors.ts
Normal file
347
lib/gui/app/modules/errors.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import {
|
||||
assign,
|
||||
flow,
|
||||
invoke,
|
||||
isEmpty,
|
||||
isError,
|
||||
isNil,
|
||||
isPlainObject,
|
||||
isString,
|
||||
toString,
|
||||
trim,
|
||||
} from 'lodash';
|
||||
|
||||
import { Dict } from './utils';
|
||||
|
||||
const INDENTATION_SPACES = 2;
|
||||
|
||||
/**
|
||||
* @summary Human-friendly error messages
|
||||
*/
|
||||
export const HUMAN_FRIENDLY: Dict<{
|
||||
title: (error?: { path?: string }) => string;
|
||||
description: (error?: any) => string;
|
||||
}> = {
|
||||
ENOENT: {
|
||||
title: (error: { path: string }) =>
|
||||
`No such file or directory: ${error.path}`,
|
||||
description: () => "The file you're trying to access doesn't exist",
|
||||
},
|
||||
EPERM: {
|
||||
title: () => "You're not authorized to perform this operation",
|
||||
description: () =>
|
||||
'Please ensure you have necessary permissions for this task',
|
||||
},
|
||||
EACCES: {
|
||||
title: () => "You don't have access to this resource",
|
||||
description: () =>
|
||||
'Please ensure you have necessary permissions to access this resource',
|
||||
},
|
||||
ENOMEM: {
|
||||
title: () => 'Your system ran out of memory',
|
||||
description: () =>
|
||||
'Please make sure your system has enough available memory for this task',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Get user friendly property from an error
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @param {Error} error - error
|
||||
* @param {String} property - HUMAN_FRIENDLY property
|
||||
* @returns {(String|Undefined)} user friendly message
|
||||
*
|
||||
* @example
|
||||
* const error = new Error('My error');
|
||||
* error.code = 'ENOMEM';
|
||||
*
|
||||
* const friendlyDescription = getUserFriendlyMessageProperty(error, 'description');
|
||||
*
|
||||
* if (friendlyDescription) {
|
||||
* console.log(friendlyDescription);
|
||||
* }
|
||||
*/
|
||||
function getUserFriendlyMessageProperty(
|
||||
error: { code?: string; path?: string },
|
||||
property: 'title' | 'description',
|
||||
): string | null {
|
||||
const code = error.code;
|
||||
if (!isString(code)) {
|
||||
return null;
|
||||
}
|
||||
return invoke(HUMAN_FRIENDLY, [code, property], error);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if a string is blank
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @param {String} string - string
|
||||
* @returns {Boolean} whether the string is blank
|
||||
*
|
||||
* @example
|
||||
* if (isBlank(' ')) {
|
||||
* console.log('The string is blank');
|
||||
* }
|
||||
*/
|
||||
const isBlank = flow([trim, isEmpty]);
|
||||
|
||||
/**
|
||||
* @summary Get the title of an error
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* Try to get as much information as possible about the error
|
||||
* rather than falling back to generic messages right away.
|
||||
*
|
||||
* @param {Error} error - error
|
||||
* @returns {String} error title
|
||||
*
|
||||
* @example
|
||||
* const error = new Error('Foo bar');
|
||||
* const title = errors.getTitle(error);
|
||||
* console.log(title);
|
||||
*/
|
||||
export function getTitle(error: Error | Dict<any>): string {
|
||||
if (!isError(error) && !isPlainObject(error) && !isNil(error)) {
|
||||
return toString(error);
|
||||
}
|
||||
|
||||
const codeTitle = getUserFriendlyMessageProperty(error, 'title');
|
||||
if (!isNil(codeTitle)) {
|
||||
return codeTitle;
|
||||
}
|
||||
|
||||
const message = error.message;
|
||||
if (!isBlank(message)) {
|
||||
return message;
|
||||
}
|
||||
|
||||
const code = error.code;
|
||||
if (!isNil(code) && !isBlank(code)) {
|
||||
return `Error code: ${code}`;
|
||||
}
|
||||
|
||||
return 'An error ocurred';
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get the description of an error
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Error} error - error
|
||||
* @returns {String} error description
|
||||
*
|
||||
* @example
|
||||
* const error = new Error('Foo bar');
|
||||
* const description = errors.getDescription(error);
|
||||
* console.log(description);
|
||||
*/
|
||||
export function getDescription(error: {
|
||||
code?: string;
|
||||
description?: string;
|
||||
stack?: string;
|
||||
}): string {
|
||||
if (!isError(error) && !isPlainObject(error)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!isBlank(error.description)) {
|
||||
return error.description as string;
|
||||
}
|
||||
|
||||
const codeDescription = getUserFriendlyMessageProperty(error, 'description');
|
||||
if (!isNil(codeDescription)) {
|
||||
return codeDescription;
|
||||
}
|
||||
|
||||
if (error.stack) {
|
||||
return error.stack;
|
||||
}
|
||||
|
||||
if (isEmpty(error)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return JSON.stringify(error, null, INDENTATION_SPACES);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Create an error
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Object} options - options
|
||||
* @param {String} options.title - error title
|
||||
* @param {String} [options.description] - error description
|
||||
* @param {Boolean} [options.report] - report error
|
||||
* @returns {Error} error
|
||||
*
|
||||
* @example
|
||||
* const error = errors.createError({
|
||||
* title: 'Foo'
|
||||
* description: 'Bar'
|
||||
* });
|
||||
*
|
||||
* throw error;
|
||||
*/
|
||||
export function createError(options: {
|
||||
title: string;
|
||||
description?: string;
|
||||
report?: boolean;
|
||||
code?: string;
|
||||
}): Error & { description?: string; report?: boolean; code?: string } {
|
||||
if (isBlank(options.title)) {
|
||||
throw new Error(`Invalid error title: ${options.title}`);
|
||||
}
|
||||
|
||||
const error: Error & {
|
||||
description?: string;
|
||||
report?: boolean;
|
||||
code?: string;
|
||||
} = new Error(options.title);
|
||||
error.description = options.description;
|
||||
|
||||
if (!isNil(options.report) && !options.report) {
|
||||
error.report = false;
|
||||
}
|
||||
|
||||
if (!isNil(options.code)) {
|
||||
error.code = options.code;
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Create a user error
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* User errors represent invalid states that the user
|
||||
* caused, that are not errors on the application itself.
|
||||
* Therefore, user errors don't get reported to analytics
|
||||
* and error reporting services.
|
||||
*
|
||||
* @returns {Error} user error
|
||||
*
|
||||
* @example
|
||||
* const error = errors.createUserError({
|
||||
* title: 'Foo',
|
||||
* description: 'Bar'
|
||||
* });
|
||||
*
|
||||
* throw error;
|
||||
*/
|
||||
export function createUserError(options: {
|
||||
title: string;
|
||||
description: string;
|
||||
code?: string;
|
||||
}): Error {
|
||||
return createError({
|
||||
title: options.title,
|
||||
description: options.description,
|
||||
report: false,
|
||||
code: options.code,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if an error is an user error
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Error} error - error
|
||||
* @returns {Boolean} whether the error is a user error
|
||||
*
|
||||
* @example
|
||||
* const error = errors.createUserError('Foo', 'Bar');
|
||||
*
|
||||
* if (errors.isUserError(error)) {
|
||||
* console.log('This error is a user error');
|
||||
* }
|
||||
*/
|
||||
export function isUserError(error: { report?: boolean }): boolean {
|
||||
return isNil(error.report) ? false : !error.report;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Convert an Error object to a JSON object
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Error} error - error object
|
||||
* @returns {Object} json error
|
||||
*
|
||||
* @example
|
||||
* const error = errors.toJSON(new Error('foo'))
|
||||
*
|
||||
* console.log(error.message);
|
||||
* > 'foo'
|
||||
*/
|
||||
export function toJSON(
|
||||
error: Error & {
|
||||
description?: string;
|
||||
report?: boolean;
|
||||
code?: string;
|
||||
syscall?: string;
|
||||
errno?: string | number;
|
||||
stdout?: string;
|
||||
stderr?: string;
|
||||
device?: any;
|
||||
},
|
||||
) {
|
||||
return {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
description: error.description,
|
||||
stack: error.stack,
|
||||
report: error.report,
|
||||
code: error.code,
|
||||
syscall: error.syscall,
|
||||
errno: error.errno,
|
||||
stdout: error.stdout,
|
||||
stderr: error.stderr,
|
||||
device: error.device,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Convert a JSON object to an Error object
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Error} json - json object
|
||||
* @returns {Object} error object
|
||||
*
|
||||
* @example
|
||||
* const error = errors.fromJSON(errors.toJSON(new Error('foo')));
|
||||
*
|
||||
* console.log(error.message);
|
||||
* > 'foo'
|
||||
*/
|
||||
export function fromJSON(json: Dict<any>): Error {
|
||||
return assign(new Error(json.message), json);
|
||||
}
|
@@ -24,10 +24,12 @@ const ipc = require('node-ipc')
|
||||
const isRunningInAsar = require('electron-is-running-in-asar')
|
||||
const electron = require('electron')
|
||||
const store = require('../models/store')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../models/settings')
|
||||
const flashState = require('../models/flash-state')
|
||||
const errors = require('../../../shared/errors')
|
||||
const permissions = require('../../../shared/permissions')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const errors = require('../../../gui/app/modules/errors')
|
||||
const permissions = require('../../../gui/app/modules/permissions')
|
||||
const windowProgress = require('../os/window-progress')
|
||||
const analytics = require('../modules/analytics')
|
||||
const updateLock = require('./update-lock')
|
||||
|
@@ -26,8 +26,10 @@ const os = require('os')
|
||||
const sudoPrompt = Bluebird.promisifyAll(require('sudo-prompt'))
|
||||
const { promisify } = require('util')
|
||||
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const errors = require('./errors')
|
||||
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const { tmpFileDisposer } = require('./utils')
|
||||
|
||||
const writeFileAsync = promisify(fs.writeFile)
|
@@ -16,9 +16,11 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../models/settings')
|
||||
const utils = require('../../../shared/utils')
|
||||
const units = require('../../../shared/units')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const utils = require('../../../gui/app/modules/utils')
|
||||
const units = require('../../../gui/app/modules/units')
|
||||
|
||||
/**
|
||||
* @summary Make the progress status subtitle string
|
||||
|
100
lib/gui/app/modules/screensaver.ts
Normal file
100
lib/gui/app/modules/screensaver.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2019 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.
|
||||
*/
|
||||
|
||||
import { execFile } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import * as settings from '../models/settings';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const EVENT_TYPES = [
|
||||
'focus',
|
||||
'keydown',
|
||||
'keyup',
|
||||
'pointerdown',
|
||||
'pointermove',
|
||||
'pointerup',
|
||||
] as const;
|
||||
|
||||
function exec(
|
||||
command: string,
|
||||
...args: string[]
|
||||
): Promise<{ stdout: string; stderr: string }> {
|
||||
return execFileAsync(command, args);
|
||||
}
|
||||
|
||||
async function screenOff(): Promise<void> {
|
||||
await exec('xset', 'dpms', 'force', 'suspend');
|
||||
}
|
||||
|
||||
async function ledsOn(): Promise<void> {
|
||||
// TODO
|
||||
}
|
||||
|
||||
async function ledsOff(): Promise<void> {
|
||||
// TODO
|
||||
}
|
||||
|
||||
export async function off() {
|
||||
await Promise.all([ledsOff(), screenOff()]);
|
||||
}
|
||||
|
||||
let timeout: NodeJS.Timeout;
|
||||
let delay: number | null = null;
|
||||
|
||||
async function listener() {
|
||||
if (timeout !== undefined) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
if (delay !== null) {
|
||||
timeout = setTimeout(off, delay);
|
||||
}
|
||||
await ledsOn();
|
||||
}
|
||||
|
||||
async function setDelay($delay: number | null) {
|
||||
const listenersSetUp = delay === null;
|
||||
delay = $delay;
|
||||
if (timeout !== undefined) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
if (delay === null) {
|
||||
for (const eventType of EVENT_TYPES) {
|
||||
removeEventListener(eventType, listener);
|
||||
}
|
||||
} else {
|
||||
timeout = setTimeout(screenOff, delay);
|
||||
if (!listenersSetUp) {
|
||||
for (const eventType of EVENT_TYPES) {
|
||||
addEventListener(eventType, listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function delayValue(d?: string): number | null {
|
||||
if (d === undefined || d === 'never') {
|
||||
return null;
|
||||
}
|
||||
return parseInt(d, 10) * 60 * 1000;
|
||||
}
|
||||
|
||||
export async function init(): Promise<void> {
|
||||
setDelay(delayValue(await settings.get('screensaverDelay')));
|
||||
settings.events.on('screensaverDelay', d => {
|
||||
setDelay(delayValue(d));
|
||||
});
|
||||
}
|
@@ -21,6 +21,7 @@ const EventEmitter = require('events')
|
||||
const createInactivityTimer = require('inactivity-timer')
|
||||
const debug = require('debug')('etcher:update-lock')
|
||||
const analytics = require('./analytics')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../models/settings')
|
||||
|
||||
/* eslint-disable no-magic-numbers, callback-return */
|
||||
|
@@ -14,14 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as _ from 'lodash';
|
||||
import * as request from 'request';
|
||||
import * as tmp from 'tmp';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const _ = require('lodash')
|
||||
const Bluebird = require('bluebird')
|
||||
const request = Bluebird.promisifyAll(require('request'))
|
||||
const tmp = require('tmp')
|
||||
import * as errors from './errors';
|
||||
|
||||
const errors = require('./errors')
|
||||
const getAsync = promisify(request.get);
|
||||
|
||||
/**
|
||||
* @summary Minimum percentage value
|
||||
@@ -29,7 +30,7 @@ const errors = require('./errors')
|
||||
* @public
|
||||
* @type {Number}
|
||||
*/
|
||||
exports.PERCENTAGE_MINIMUM = 0
|
||||
export const PERCENTAGE_MINIMUM = 0;
|
||||
|
||||
/**
|
||||
* @summary Maximum percentage value
|
||||
@@ -37,7 +38,7 @@ exports.PERCENTAGE_MINIMUM = 0
|
||||
* @public
|
||||
* @type {Number}
|
||||
*/
|
||||
exports.PERCENTAGE_MAXIMUM = 100
|
||||
export const PERCENTAGE_MAXIMUM = 100;
|
||||
|
||||
/**
|
||||
* @summary Check if a percentage is valid
|
||||
@@ -52,12 +53,12 @@ exports.PERCENTAGE_MAXIMUM = 100
|
||||
* console.log('The percentage is valid');
|
||||
* }
|
||||
*/
|
||||
exports.isValidPercentage = (percentage) => {
|
||||
return _.every([
|
||||
_.isNumber(percentage),
|
||||
percentage >= exports.PERCENTAGE_MINIMUM,
|
||||
percentage <= exports.PERCENTAGE_MAXIMUM
|
||||
])
|
||||
export function isValidPercentage(percentage: number) {
|
||||
return _.every([
|
||||
_.isNumber(percentage),
|
||||
percentage >= exports.PERCENTAGE_MINIMUM,
|
||||
percentage <= exports.PERCENTAGE_MAXIMUM,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,14 +74,14 @@ exports.isValidPercentage = (percentage) => {
|
||||
* console.log(value);
|
||||
* > 0.5
|
||||
*/
|
||||
exports.percentageToFloat = (percentage) => {
|
||||
if (!exports.isValidPercentage(percentage)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid percentage: ${percentage}`
|
||||
})
|
||||
}
|
||||
export function percentageToFloat(percentage: number) {
|
||||
if (!isValidPercentage(percentage)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid percentage: ${percentage}`,
|
||||
});
|
||||
}
|
||||
|
||||
return percentage / exports.PERCENTAGE_MAXIMUM
|
||||
return percentage / PERCENTAGE_MAXIMUM;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,37 +110,40 @@ exports.percentageToFloat = (percentage) => {
|
||||
*
|
||||
* const memoizedFunction = memoize(getList, angular.equals);
|
||||
*/
|
||||
exports.memoize = (func, comparer) => {
|
||||
let previousTuples = []
|
||||
export function memoize(
|
||||
func: (...args: any[]) => any,
|
||||
comparer: (a: any, b: any) => boolean,
|
||||
) {
|
||||
let previousTuples: any[] = [];
|
||||
|
||||
return (...restArgs) => {
|
||||
let areArgsInTuple = false
|
||||
let state = Reflect.apply(func, this, restArgs)
|
||||
return (...restArgs: any[]) => {
|
||||
let areArgsInTuple = false;
|
||||
let state = Reflect.apply(func, this, restArgs);
|
||||
|
||||
previousTuples = _.map(previousTuples, ([ oldArgs, oldState ]) => {
|
||||
if (comparer(oldArgs, restArgs)) {
|
||||
areArgsInTuple = true
|
||||
previousTuples = _.map(previousTuples, ([oldArgs, oldState]) => {
|
||||
if (comparer(oldArgs, restArgs)) {
|
||||
areArgsInTuple = true;
|
||||
|
||||
if (comparer(state, oldState)) {
|
||||
// Use the previously memoized state for this argument
|
||||
state = oldState
|
||||
}
|
||||
if (comparer(state, oldState)) {
|
||||
// Use the previously memoized state for this argument
|
||||
state = oldState;
|
||||
}
|
||||
|
||||
// Update the tuple state
|
||||
return [ oldArgs, state ]
|
||||
}
|
||||
// Update the tuple state
|
||||
return [oldArgs, state];
|
||||
}
|
||||
|
||||
// Return the tuple unchanged
|
||||
return [ oldArgs, oldState ]
|
||||
})
|
||||
// Return the tuple unchanged
|
||||
return [oldArgs, oldState];
|
||||
});
|
||||
|
||||
// Add the state associated with these args to be memoized
|
||||
if (!areArgsInTuple) {
|
||||
previousTuples.push([ restArgs, state ])
|
||||
}
|
||||
// Add the state associated with these args to be memoized
|
||||
if (!areArgsInTuple) {
|
||||
previousTuples.push([restArgs, state]);
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
return state;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,20 +159,19 @@ exports.memoize = (func, comparer) => {
|
||||
* @example
|
||||
* const doesIt = hasProps({ foo: 'bar' }, [ 'foo' ]);
|
||||
*/
|
||||
exports.hasProps = (obj, props) => {
|
||||
return _.every(props, (prop) => {
|
||||
return _.has(obj, prop)
|
||||
})
|
||||
export function hasProps(obj: any, props: string[]) {
|
||||
return _.every(props, prop => {
|
||||
return _.has(obj, prop);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get etcher configs stored online
|
||||
* @param {String} - url where config.json is stored
|
||||
*/
|
||||
// eslint-disable-next-line
|
||||
exports.getConfig = (configUrl) => {
|
||||
return request.getAsync(configUrl, { json: true })
|
||||
.get('body')
|
||||
* @summary Get etcher configs stored online
|
||||
* @param {String} - url where config.json is stored
|
||||
*/
|
||||
export async function getConfig(configUrl: string) {
|
||||
// @ts-ignore
|
||||
return (await getAsync(configUrl, { json: true })).body;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,16 +189,16 @@ exports.getConfig = (configUrl) => {
|
||||
* cleanup()
|
||||
* });
|
||||
*/
|
||||
const tmpFileAsync = (options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
tmp.file(options, (error, path, _fd, cleanup) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve({ path, cleanup })
|
||||
}
|
||||
})
|
||||
})
|
||||
function tmpFileAsync(options: tmp.FileOptions) {
|
||||
return new Promise((resolve, reject) => {
|
||||
tmp.file(options, (error, path, _fd, cleanup) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve({ path, cleanup });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,9 +214,12 @@ const tmpFileAsync = (options) => {
|
||||
* console.log(path);
|
||||
* })
|
||||
*/
|
||||
exports.tmpFileDisposer = (options) => {
|
||||
return Bluebird.resolve(tmpFileAsync(options))
|
||||
.disposer(({ cleanup }) => {
|
||||
cleanup()
|
||||
})
|
||||
export function tmpFileDisposer(options: tmp.FileOptions) {
|
||||
return Bluebird.resolve(tmpFileAsync(options)).disposer(({ cleanup }) => {
|
||||
cleanup();
|
||||
});
|
||||
}
|
||||
|
||||
export interface Dict<T> {
|
||||
[key: string]: T;
|
||||
}
|
@@ -19,8 +19,9 @@
|
||||
const _ = require('lodash')
|
||||
const electron = require('electron')
|
||||
const Bluebird = require('bluebird')
|
||||
const errors = require('../../../shared/errors')
|
||||
const supportedFormats = require('../../../shared/supported-formats')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const errors = require('../../../gui/app/modules/errors')
|
||||
const supportedFormats = require('../../../gui/app/modules/supported-formats')
|
||||
|
||||
/**
|
||||
* @summary Current renderer BrowserWindow instance
|
||||
|
@@ -17,6 +17,7 @@
|
||||
'use strict'
|
||||
|
||||
const electron = require('electron')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../models/settings')
|
||||
|
||||
/**
|
||||
|
@@ -19,6 +19,7 @@
|
||||
const electron = require('electron')
|
||||
const store = require('../../../models/store')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../../../models/settings')
|
||||
|
||||
module.exports = function () {
|
||||
@@ -34,7 +35,7 @@ module.exports = function () {
|
||||
*/
|
||||
this.open = (url) => {
|
||||
// Don't open links if they're disabled by the env var
|
||||
if (settings.get('disableExternalLinks')) {
|
||||
if (settings.get('disableExternalLinks') || !url) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@@ -17,7 +17,8 @@
|
||||
'use strict'
|
||||
|
||||
const electron = require('electron')
|
||||
const utils = require('../../../shared/utils')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const utils = require('../../../gui/app/modules/utils')
|
||||
const progressStatus = require('../modules/progress-status')
|
||||
|
||||
/**
|
||||
|
@@ -25,7 +25,8 @@ const Path = require('path')
|
||||
const process = require('process')
|
||||
const { promisify } = require('util')
|
||||
|
||||
const { tmpFileDisposer } = require('../../../shared/utils')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const { tmpFileDisposer } = require('../../../gui/app/modules/utils')
|
||||
|
||||
const readFileAsync = promisify(fs.readFile)
|
||||
|
||||
|
@@ -19,12 +19,13 @@
|
||||
const _ = require('lodash')
|
||||
const uuidV4 = require('uuid/v4')
|
||||
const store = require('../../../models/store')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../../../models/settings')
|
||||
const flashState = require('../../../models/flash-state')
|
||||
const selectionState = require('../../../models/selection-state')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
const updateLock = require('../../../modules/update-lock')
|
||||
const messages = require('../../../../../shared/messages')
|
||||
const messages = require('../../../../../gui/app/modules/messages')
|
||||
|
||||
module.exports = function ($state) {
|
||||
/**
|
||||
|
@@ -20,13 +20,19 @@ const _ = require('lodash')
|
||||
const angular = require('angular')
|
||||
const prettyBytes = require('pretty-bytes')
|
||||
const store = require('../../../models/store')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../../../models/settings')
|
||||
const availableDrives = require('../../../models/available-drives')
|
||||
const selectionState = require('../../../models/selection-state')
|
||||
const driveConstraints = require('../../../modules/drive-constraints')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
const exceptionReporter = require('../../../modules/exception-reporter')
|
||||
const utils = require('../../../../../shared/utils')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const utils = require('../../../../../gui/app/modules/utils')
|
||||
|
||||
module.exports = function ($timeout, DriveSelectorService) {
|
||||
this.driveSelectorModalOpen = false;
|
||||
|
||||
module.exports = function (DriveSelectorService) {
|
||||
/**
|
||||
* @summary Get drive title based on device quantity
|
||||
* @function
|
||||
@@ -101,20 +107,23 @@ module.exports = function (DriveSelectorService) {
|
||||
* DriveSelectionController.openDriveSelector();
|
||||
*/
|
||||
this.openDriveSelector = () => {
|
||||
DriveSelectorService.open().then((drive) => {
|
||||
if (!drive) {
|
||||
return
|
||||
}
|
||||
this.driveSelectorModalOpen = true;
|
||||
// Trigger re-render
|
||||
$timeout()
|
||||
//DriveSelectorService.open().then((drive) => {
|
||||
// if (!drive) {
|
||||
// return
|
||||
// }
|
||||
|
||||
selectionState.selectDrive(drive.device)
|
||||
// selectionState.selectDrive(drive.device)
|
||||
|
||||
analytics.logEvent('Select drive', {
|
||||
device: drive.device,
|
||||
unsafeMode: settings.get('unsafeMode') && !settings.get('disableUnsafeMode'),
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
}).catch(exceptionReporter.report)
|
||||
// analytics.logEvent('Select drive', {
|
||||
// device: drive.device,
|
||||
// unsafeMode: settings.get('unsafeMode') && !settings.get('disableUnsafeMode'),
|
||||
// applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
// flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
// })
|
||||
//}).catch(exceptionReporter.report)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,4 +165,34 @@ module.exports = function (DriveSelectorService) {
|
||||
this.shouldShowDrivesButton = () => {
|
||||
return !settings.get('disableExplicitDriveSelection')
|
||||
}
|
||||
|
||||
this.closeDriveSelectorModal = () => {
|
||||
this.driveSelectorModalOpen = false
|
||||
// Trigger re-render
|
||||
$timeout()
|
||||
}
|
||||
|
||||
this.setSelectedDrives = (drives) => {
|
||||
const devices = drives.map(d => d.device);
|
||||
for (const drive of availableDrives.getDrives()) {
|
||||
if (devices.indexOf(drive.device) !== -1) {
|
||||
selectionState.selectDrive(drive.device)
|
||||
} else {
|
||||
selectionState.deselectDrive(drive.device)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.isDriveSelected = (drive) => {
|
||||
return selectionState.isDriveSelected(drive.device)
|
||||
}
|
||||
|
||||
this.isDriveValid = (drive) => {
|
||||
return driveConstraints.isDriveValid(drive, selectionState.getImage());
|
||||
}
|
||||
|
||||
this.getDriveBadges = (drive) => {
|
||||
return driveConstraints.getDriveImageCompatibilityStatuses(drive, selectionState.getImage());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@
|
||||
'use strict'
|
||||
|
||||
const _ = require('lodash')
|
||||
const messages = require('../../../../../shared/messages')
|
||||
const messages = require('../../../../../gui/app/modules/messages')
|
||||
const flashState = require('../../../models/flash-state')
|
||||
const driveScanner = require('../../../modules/drive-scanner')
|
||||
const progressStatus = require('../../../modules/progress-status')
|
||||
@@ -26,7 +26,7 @@ const analytics = require('../../../modules/analytics')
|
||||
const imageWriter = require('../../../modules/image-writer')
|
||||
const path = require('path')
|
||||
const store = require('../../../models/store')
|
||||
const constraints = require('../../../../../shared/drive-constraints')
|
||||
const constraints = require('../../../../../gui/app/modules/drive-constraints')
|
||||
const availableDrives = require('../../../models/available-drives')
|
||||
const selection = require('../../../models/selection-state')
|
||||
|
||||
|
@@ -22,10 +22,12 @@ const path = require('path')
|
||||
const sdk = require('etcher-sdk')
|
||||
|
||||
const store = require('../../../models/store')
|
||||
const messages = require('../../../../../shared/messages')
|
||||
const errors = require('../../../../../shared/errors')
|
||||
const supportedFormats = require('../../../../../shared/supported-formats')
|
||||
const messages = require('../../../../../gui/app/modules/messages')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const errors = require('../../../../../gui/app/modules/errors')
|
||||
const supportedFormats = require('../../../../../gui/app/modules/supported-formats')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../../../models/settings')
|
||||
const selectionState = require('../../../models/selection-state')
|
||||
const osDialog = require('../../../os/dialog')
|
||||
@@ -245,6 +247,11 @@ module.exports = function (
|
||||
this.openImageSelector()
|
||||
}
|
||||
|
||||
this.deselectImage = () => {
|
||||
selectionState.deselectImage()
|
||||
$timeout()
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get the basename of the selected image
|
||||
* @function
|
||||
@@ -262,4 +269,54 @@ module.exports = function (
|
||||
|
||||
return path.basename(selectionState.getImagePath())
|
||||
}
|
||||
|
||||
this.driveSelectorModalOpen = false
|
||||
|
||||
this.openDriveSelector = () => {
|
||||
this.driveSelectorModalOpen = true;
|
||||
$timeout()
|
||||
}
|
||||
|
||||
this.closeDriveSelectorModal = () => {
|
||||
this.driveSelectorModalOpen = false;
|
||||
$timeout()
|
||||
}
|
||||
|
||||
this.setSelectedDrives = (drives) => {
|
||||
const currentlySelected = this.getSelectedDrive()
|
||||
if (currentlySelected) {
|
||||
drives = drives.filter(d => d.device !== currentlySelected.path)
|
||||
}
|
||||
if (drives.length === 0) {
|
||||
this.deselectImage()
|
||||
} else {
|
||||
selectionState.selectImage({
|
||||
path: drives[0].device,
|
||||
size: drives[0].size,
|
||||
isDrive: true
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
this.getSelectedDrive = () => {
|
||||
const image = selectionState.getImage()
|
||||
if (image && image.isDrive) {
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
||||
this.isDriveSelected = (drive) => {
|
||||
const selectedDrive = this.getSelectedDrive()
|
||||
return selectedDrive && selectedDrive.path === drive.device
|
||||
}
|
||||
|
||||
this.isDriveValid = (drive) => {
|
||||
return true // TODO: not valid if already a destination drive
|
||||
}
|
||||
|
||||
this.getDriveBadges = (drive) => {
|
||||
return [] // TODO: selected as destination (same as above)
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -18,14 +18,15 @@
|
||||
|
||||
const path = require('path')
|
||||
const store = require('../../../models/store')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../../../models/settings')
|
||||
const flashState = require('../../../models/flash-state')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
const exceptionReporter = require('../../../modules/exception-reporter')
|
||||
const availableDrives = require('../../../models/available-drives')
|
||||
const selectionState = require('../../../models/selection-state')
|
||||
const driveConstraints = require('../../../../../shared/drive-constraints')
|
||||
const messages = require('../../../../../shared/messages')
|
||||
const driveConstraints = require('../../../../../gui/app/modules/drive-constraints')
|
||||
const messages = require('../../../../../gui/app/modules/messages')
|
||||
const prettyBytes = require('pretty-bytes')
|
||||
|
||||
module.exports = function (
|
||||
|
@@ -34,6 +34,7 @@ const MainPage = angular.module(MODULE_NAME, [
|
||||
require('angular-seconds-to-date'),
|
||||
|
||||
require('../../components/drive-selector/drive-selector'),
|
||||
require('../../components/drive-selector2'),
|
||||
require('../../components/tooltip-modal/tooltip-modal'),
|
||||
require('../../components/flash-error-modal/flash-error-modal'),
|
||||
require('../../components/progress-button'),
|
||||
|
@@ -1,5 +1,15 @@
|
||||
<div class="page-main row around-xs">
|
||||
<div class="col-xs" ng-controller="ImageSelectionController as image">
|
||||
<drive-selector-2
|
||||
ng-if="image.driveSelectorModalOpen"
|
||||
title="'Select a source drive'"
|
||||
close="image.closeDriveSelectorModal"
|
||||
set-selected-drives="image.setSelectedDrives"
|
||||
is-drive-selected="image.isDriveSelected"
|
||||
is-drive-valid="image.isDriveValid"
|
||||
get-drive-badges="image.getDriveBadges"
|
||||
>
|
||||
</drive-selector-2>
|
||||
<div class="box text-center relative" os-dropzone="image.selectImageByPath($file)">
|
||||
|
||||
<div class="center-block">
|
||||
@@ -10,12 +20,13 @@
|
||||
<image-selector
|
||||
has-image="main.selection.hasImage()"
|
||||
open-image-selector="image.openImageSelector"
|
||||
open-drive-selector="image.openDriveSelector"
|
||||
main-supported-extensions="image.mainSupportedExtensions"
|
||||
extra-supported-extensions="image.extraSupportedExtensions"
|
||||
show-selected-image-details="main.showSelectedImageDetails"
|
||||
image-name="main.selection.getImageName()"
|
||||
image-basename="image.getImageBasename()"
|
||||
reselect-image="image.reselectImage"
|
||||
deselect-image="image.deselectImage"
|
||||
flashing="main.state.isFlashing()"
|
||||
image-size="main.selection.getImageSize()"
|
||||
>
|
||||
@@ -26,6 +37,16 @@
|
||||
</div>
|
||||
|
||||
<div class="col-xs" ng-controller="DriveSelectionController as drive">
|
||||
<drive-selector-2
|
||||
ng-if="drive.driveSelectorModalOpen"
|
||||
title="'Available targets'"
|
||||
close="drive.closeDriveSelectorModal"
|
||||
set-selected-drives="drive.setSelectedDrives"
|
||||
is-drive-selected="drive.isDriveSelected"
|
||||
is-drive-valid="drive.isDriveValid"
|
||||
get-drive-badges="drive.getDriveBadges"
|
||||
>
|
||||
</drive-selector-2>
|
||||
<div class="box text-center relative">
|
||||
|
||||
<div class="step-border-left" ng-disabled="main.shouldDriveStepBeDisabled()" ng-hide="main.state.isFlashing() && main.isWebviewShowing"></div>
|
||||
|
@@ -19,6 +19,7 @@
|
||||
const os = require('os')
|
||||
const _ = require('lodash')
|
||||
const store = require('../../../models/store')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../../../models/settings')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
const exceptionReporter = require('../../../modules/exception-reporter')
|
||||
@@ -107,6 +108,16 @@ module.exports = function (WarningModalService) {
|
||||
}).catch(exceptionReporter.report)
|
||||
}
|
||||
|
||||
this.set = (setting, value) => {
|
||||
analytics.logEvent('Set setting', {
|
||||
setting,
|
||||
value,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid
|
||||
})
|
||||
this.model.set(setting, value)
|
||||
this.refreshSettings()
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Show unsafe mode based on an env var
|
||||
* @function
|
||||
@@ -120,4 +131,19 @@ module.exports = function (WarningModalService) {
|
||||
this.shouldShowUnsafeMode = () => {
|
||||
return !settings.get('disableUnsafeMode')
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Show the screensaverDelay setting
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* SettingsController.shouldShowScreensaverDelay()
|
||||
*/
|
||||
this.shouldShowScreensaverDelay = () => {
|
||||
return settings.get('showScreensaverDelay')
|
||||
}
|
||||
}
|
||||
|
@@ -77,4 +77,41 @@
|
||||
<span>Unsafe mode <span class="label label-danger">Dangerous</span></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div ng-if="settings.shouldShowScreensaverDelay()">
|
||||
<h4>
|
||||
Screensaver
|
||||
</h4>
|
||||
<label>
|
||||
<input
|
||||
ng-model="settings.currentData.screensaverDelay"
|
||||
ng-change="settings.set('screensaverDelay', '5')"
|
||||
type="radio"
|
||||
value="5"
|
||||
tabindex="11"
|
||||
>
|
||||
5 min
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
ng-model="settings.currentData.screensaverDelay"
|
||||
ng-change="settings.set('screensaverDelay', '10')"
|
||||
type="radio"
|
||||
value="10"
|
||||
tabindex="12"
|
||||
>
|
||||
10 min
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
ng-model="settings.currentData.screensaverDelay"
|
||||
ng-change="settings.set('screensaverDelay', 'never')"
|
||||
type="radio"
|
||||
value="never"
|
||||
tabindex="13"
|
||||
>
|
||||
never
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@@ -45,6 +45,7 @@ $fa-font-path: "../../../node_modules/@fortawesome/fontawesome-free-webfonts/web
|
||||
|
||||
@import "../../../../node_modules/@fortawesome/fontawesome-free-webfonts/scss/fontawesome";
|
||||
@import "../../../../node_modules/@fortawesome/fontawesome-free-webfonts/scss/fa-solid";
|
||||
@import "../../../../node_modules/@fortawesome/fontawesome-free-webfonts/scss/fa-regular";
|
||||
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
@@ -238,3 +239,19 @@ featured-project {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.sleep-button {
|
||||
float: left;
|
||||
background-color: #3c3e42;
|
||||
width: 76px;
|
||||
height: 24px;
|
||||
border-radius: 24px;
|
||||
padding: 0px;
|
||||
margin-top: 10px;
|
||||
margin-left: 6px;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
background-color: #414347;
|
||||
}
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
const units = require('../../../../shared/units')
|
||||
const units = require('../../../../gui/app/modules/units')
|
||||
|
||||
module.exports = () => {
|
||||
/**
|
||||
|
@@ -16,7 +16,8 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
const errors = require('../../../../../shared/errors')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const errors = require('../../../../../gui/app/modules/errors')
|
||||
|
||||
/**
|
||||
* @summary ManifestBind directive
|
||||
|
3
lib/gui/assets/moon.svg
Normal file
3
lib/gui/assets/moon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="#D3D6DB" fill-rule="evenodd" stroke="#D3D6DB" stroke-linecap="round" d="M5.313 1.002a.173.173 0 0 0-.038.01C2.559 2.02 1 4.642 1 7.686a7.12 7.12 0 0 0 7.116 7.116c3.044 0 5.667-1.565 6.673-4.28a.173.173 0 0 0-.226-.221c-.77.309-1.607.468-2.49.468-3.686 0-7.046-3.354-7.046-7.04 0-.883.16-1.72.469-2.49a.173.173 0 0 0-.183-.237zm-.248.49a7.033 7.033 0 0 0-.383 2.237c0 3.894 3.497 7.386 7.39 7.386.786 0 1.53-.147 2.237-.383-1.04 2.362-3.399 3.725-6.193 3.725-3.741 0-6.771-3.03-6.771-6.77 0-2.794 1.36-5.154 3.72-6.195z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 630 B |
@@ -9860,6 +9860,17 @@ readers do not read off random characters that represent icons */
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 900; }
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url("../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts/fa-regular-400.eot");
|
||||
src: url("../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts/fa-regular-400.woff2") format("woff2"), url("../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts/fa-regular-400.woff") format("woff"), url("../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts/fa-regular-400.ttf") format("truetype"), url("../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts/fa-regular-400.svg#fontawesome") format("svg"); }
|
||||
|
||||
.far {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 400; }
|
||||
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
src: url("Nunito-Regular.eot");
|
||||
@@ -9992,3 +10003,16 @@ featured-project.fp-visible webview {
|
||||
top: 45px;
|
||||
border-radius: 7px;
|
||||
overflow: hidden; }
|
||||
|
||||
.sleep-button {
|
||||
float: left;
|
||||
background-color: #3c3e42;
|
||||
width: 76px;
|
||||
height: 24px;
|
||||
border-radius: 24px;
|
||||
padding: 0px;
|
||||
margin-top: 10px;
|
||||
margin-left: 6px;
|
||||
font-size: 12px; }
|
||||
.sleep-button:hover {
|
||||
background-color: #414347; }
|
||||
|
@@ -21,14 +21,14 @@ const path = require('path')
|
||||
const _ = require('lodash')
|
||||
const { autoUpdater } = require('electron-updater')
|
||||
const Bluebird = require('bluebird')
|
||||
const EXIT_CODES = require('../shared/exit-codes')
|
||||
const EXIT_CODES = require('./app/modules/exit-codes')
|
||||
const buildWindowMenu = require('./menu')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('./app/models/settings')
|
||||
const analytics = require('./app/modules/analytics')
|
||||
const { getConfig } = require('../shared/utils')
|
||||
/* eslint-disable lodash/prefer-lodash-method */
|
||||
|
||||
const config = settings.getDefaults()
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const { getConfig } = require('./app/modules/utils')
|
||||
/* eslint-disable lodash/prefer-lodash-method,func-style,space-before-function-paren,require-jsdoc */
|
||||
|
||||
const configUrl = settings.get('configUrl') || 'https://balena.io/etcher/static/config.json'
|
||||
|
||||
@@ -55,10 +55,11 @@ const checkForUpdates = async (interval) => {
|
||||
|
||||
/**
|
||||
* @summary Create Etcher's main window
|
||||
* @param {Object} config - config
|
||||
* @example
|
||||
* electron.app.on('ready', createMainWindow)
|
||||
*/
|
||||
const createMainWindow = () => {
|
||||
const createMainWindow = (config) => {
|
||||
const mainWindow = new electron.BrowserWindow({
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
width: parseInt(config.width, 10) || 800,
|
||||
@@ -139,18 +140,17 @@ electron.app.on('before-quit', () => {
|
||||
process.exit(EXIT_CODES.SUCCESS)
|
||||
})
|
||||
|
||||
settings.load().then((localSettings) => {
|
||||
Object.assign(config, localSettings)
|
||||
}).catch((error) => {
|
||||
// TODO: What do if loading the config fails?
|
||||
console.error('Error loading settings:')
|
||||
console.error(error)
|
||||
}).finally(() => {
|
||||
async function main() {
|
||||
const config = settings.load()
|
||||
if (electron.app.isReady()) {
|
||||
createMainWindow()
|
||||
createMainWindow(config)
|
||||
} else {
|
||||
electron.app.on('ready', createMainWindow)
|
||||
electron.app.on('ready', () => {
|
||||
createMainWindow(config)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
console.time('ready-to-show')
|
||||
|
@@ -1,369 +0,0 @@
|
||||
/*
|
||||
* 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 _ = require('lodash')
|
||||
|
||||
/**
|
||||
* @summary Create an error details object
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @param {Object} options - options
|
||||
* @param {(String|Function)} options.title - error title
|
||||
* @param {(String|Function)} options.description - error description
|
||||
* @returns {Object} error details object
|
||||
*
|
||||
* @example
|
||||
* const details = createErrorDetails({
|
||||
* title: (error) => {
|
||||
* return `An error happened, the code is ${error.code}`;
|
||||
* },
|
||||
* description: 'This is the error description'
|
||||
* });
|
||||
*/
|
||||
const createErrorDetails = (options) => {
|
||||
return _.pick(_.mapValues(options, (value) => {
|
||||
return _.isFunction(value) ? value : _.constant(value)
|
||||
}), [ 'title', 'description' ])
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Human-friendly error messages
|
||||
* @namespace HUMAN_FRIENDLY
|
||||
* @public
|
||||
*/
|
||||
exports.HUMAN_FRIENDLY = {
|
||||
|
||||
/* eslint-disable new-cap */
|
||||
|
||||
/**
|
||||
* @namespace ENOENT
|
||||
* @memberof HUMAN_FRIENDLY
|
||||
*/
|
||||
ENOENT: createErrorDetails({
|
||||
title: (error) => {
|
||||
return `No such file or directory: ${error.path}`
|
||||
},
|
||||
description: 'The file you\'re trying to access doesn\'t exist'
|
||||
}),
|
||||
|
||||
/**
|
||||
* @namespace EPERM
|
||||
* @memberof HUMAN_FRIENDLY
|
||||
*/
|
||||
EPERM: createErrorDetails({
|
||||
title: 'You\'re not authorized to perform this operation',
|
||||
description: 'Please ensure you have necessary permissions for this task'
|
||||
}),
|
||||
|
||||
/**
|
||||
* @namespace EACCES
|
||||
* @memberof HUMAN_FRIENDLY
|
||||
*/
|
||||
EACCES: createErrorDetails({
|
||||
title: 'You don\'t have access to this resource',
|
||||
description: 'Please ensure you have necessary permissions to access this resource'
|
||||
}),
|
||||
|
||||
/**
|
||||
* @namespace ENOMEM
|
||||
* @memberof HUMAN_FRIENDLY
|
||||
*/
|
||||
ENOMEM: createErrorDetails({
|
||||
title: 'Your system ran out of memory',
|
||||
description: 'Please make sure your system has enough available memory for this task'
|
||||
})
|
||||
|
||||
/* eslint-enable new-cap */
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get user friendly property from an error
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @param {Error} error - error
|
||||
* @param {String} property - HUMAN_FRIENDLY property
|
||||
* @returns {(String|Undefined)} user friendly message
|
||||
*
|
||||
* @example
|
||||
* const error = new Error('My error');
|
||||
* error.code = 'ENOMEM';
|
||||
*
|
||||
* const friendlyDescription = getUserFriendlyMessageProperty(error, 'description');
|
||||
*
|
||||
* if (friendlyDescription) {
|
||||
* console.log(friendlyDescription);
|
||||
* }
|
||||
*/
|
||||
const getUserFriendlyMessageProperty = (error, property) => {
|
||||
const code = _.get(error, [ 'code' ])
|
||||
|
||||
if (_.isNil(code) || !_.isString(code)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return _.invoke(exports.HUMAN_FRIENDLY, [ code, property ], error)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if a string is blank
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @param {String} string - string
|
||||
* @returns {Boolean} whether the string is blank
|
||||
*
|
||||
* @example
|
||||
* if (isBlank(' ')) {
|
||||
* console.log('The string is blank');
|
||||
* }
|
||||
*/
|
||||
const isBlank = _.flow([ _.trim, _.isEmpty ])
|
||||
|
||||
/**
|
||||
* @summary Get the title of an error
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* Try to get as much information as possible about the error
|
||||
* rather than falling back to generic messages right away.
|
||||
*
|
||||
* @param {Error} error - error
|
||||
* @returns {String} error title
|
||||
*
|
||||
* @example
|
||||
* const error = new Error('Foo bar');
|
||||
* const title = errors.getTitle(error);
|
||||
* console.log(title);
|
||||
*/
|
||||
exports.getTitle = (error) => {
|
||||
if (!_.isError(error) && !_.isPlainObject(error) && !_.isNil(error)) {
|
||||
return _.toString(error)
|
||||
}
|
||||
|
||||
const codeTitle = getUserFriendlyMessageProperty(error, 'title')
|
||||
if (!_.isNil(codeTitle)) {
|
||||
return codeTitle
|
||||
}
|
||||
|
||||
const message = _.get(error, [ 'message' ])
|
||||
if (!isBlank(message)) {
|
||||
return message
|
||||
}
|
||||
|
||||
const code = _.get(error, [ 'code' ])
|
||||
if (!_.isNil(code) && !isBlank(code)) {
|
||||
return `Error code: ${code}`
|
||||
}
|
||||
|
||||
return 'An error ocurred'
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get the description of an error
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Error} error - error
|
||||
* @param {Object} options - options
|
||||
* @param {Boolean} [options.userFriendlyDescriptionsOnly=false] - only return user friendly descriptions
|
||||
* @returns {String} error description
|
||||
*
|
||||
* @example
|
||||
* const error = new Error('Foo bar');
|
||||
* const description = errors.getDescription(error);
|
||||
* console.log(description);
|
||||
*/
|
||||
exports.getDescription = (error, options = {}) => {
|
||||
_.defaults(options, {
|
||||
userFriendlyDescriptionsOnly: false
|
||||
})
|
||||
|
||||
if (!_.isError(error) && !_.isPlainObject(error)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!isBlank(error.description)) {
|
||||
return error.description
|
||||
}
|
||||
|
||||
const codeDescription = getUserFriendlyMessageProperty(error, 'description')
|
||||
if (!_.isNil(codeDescription)) {
|
||||
return codeDescription
|
||||
}
|
||||
|
||||
if (options.userFriendlyDescriptionsOnly) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (error.stack) {
|
||||
return error.stack
|
||||
}
|
||||
|
||||
if (_.isEmpty(error)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const INDENTATION_SPACES = 2
|
||||
return JSON.stringify(error, null, INDENTATION_SPACES)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Create an error
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Object} options - options
|
||||
* @param {String} options.title - error title
|
||||
* @param {String} [options.description] - error description
|
||||
* @param {Boolean} [options.report] - report error
|
||||
* @returns {Error} error
|
||||
*
|
||||
* @example
|
||||
* const error = errors.createError({
|
||||
* title: 'Foo'
|
||||
* description: 'Bar'
|
||||
* });
|
||||
*
|
||||
* throw error;
|
||||
*/
|
||||
exports.createError = (options) => {
|
||||
if (isBlank(options.title)) {
|
||||
throw new Error(`Invalid error title: ${options.title}`)
|
||||
}
|
||||
|
||||
const error = new Error(options.title)
|
||||
error.description = options.description
|
||||
|
||||
if (!_.isNil(options.report) && !options.report) {
|
||||
error.report = false
|
||||
}
|
||||
|
||||
if (!_.isNil(options.code)) {
|
||||
error.code = options.code
|
||||
}
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Create a user error
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* User errors represent invalid states that the user
|
||||
* caused, that are not errors on the application itself.
|
||||
* Therefore, user errors don't get reported to analytics
|
||||
* and error reporting services.
|
||||
*
|
||||
* @param {Object} options - options
|
||||
* @param {String} options.title - error title
|
||||
* @param {String} [options.description] - error description
|
||||
* @returns {Error} user error
|
||||
*
|
||||
* @example
|
||||
* const error = errors.createUserError({
|
||||
* title: 'Foo',
|
||||
* description: 'Bar'
|
||||
* });
|
||||
*
|
||||
* throw error;
|
||||
*/
|
||||
exports.createUserError = (options) => {
|
||||
return exports.createError({
|
||||
title: options.title,
|
||||
description: options.description,
|
||||
report: false,
|
||||
code: options.code
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if an error is an user error
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Error} error - error
|
||||
* @returns {Boolean} whether the error is a user error
|
||||
*
|
||||
* @example
|
||||
* const error = errors.createUserError('Foo', 'Bar');
|
||||
*
|
||||
* if (errors.isUserError(error)) {
|
||||
* console.log('This error is a user error');
|
||||
* }
|
||||
*/
|
||||
exports.isUserError = (error) => {
|
||||
return _.isNil(error.report) ? false : !error.report
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Convert an Error object to a JSON object
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Error} error - error object
|
||||
* @returns {Object} json error
|
||||
*
|
||||
* @example
|
||||
* const error = errors.toJSON(new Error('foo'))
|
||||
*
|
||||
* console.log(error.message);
|
||||
* > 'foo'
|
||||
*/
|
||||
exports.toJSON = (error) => {
|
||||
// Handle string error objects to be on the safe side
|
||||
const isErrorLike = _.isError(error) || _.isPlainObject(error)
|
||||
const errorObject = isErrorLike ? error : new Error(error)
|
||||
|
||||
return {
|
||||
name: errorObject.name,
|
||||
message: errorObject.message,
|
||||
description: errorObject.description,
|
||||
stack: errorObject.stack,
|
||||
report: errorObject.report,
|
||||
code: errorObject.code,
|
||||
syscall: errorObject.syscall,
|
||||
errno: errorObject.errno,
|
||||
stdout: errorObject.stdout,
|
||||
stderr: errorObject.stderr,
|
||||
device: errorObject.device
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Convert a JSON object to an Error object
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Error} json - json object
|
||||
* @returns {Object} error object
|
||||
*
|
||||
* @example
|
||||
* const error = errors.fromJSON(errors.toJSON(new Error('foo')));
|
||||
*
|
||||
* console.log(error.message);
|
||||
* > 'foo'
|
||||
*/
|
||||
exports.fromJSON = (json) => {
|
||||
return _.assign(new Error(json.message), json)
|
||||
}
|
@@ -27,7 +27,7 @@
|
||||
// an older equivalent of `ELECTRON_RUN_AS_NODE` that still gets set when
|
||||
// using `child_process.fork()`.
|
||||
if (process.env.ELECTRON_RUN_AS_NODE || process.env.ATOM_SHELL_INTERNAL_RUN_AS_NODE) {
|
||||
require('./gui/modules/child-writer')
|
||||
require('../generated/child-writer')
|
||||
} else {
|
||||
require('../generated/etcher')
|
||||
}
|
||||
|
1660
npm-shrinkwrap.json
generated
1660
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -20,6 +20,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"test": "make lint test sanity-checks",
|
||||
"prettier": "prettier --config ./node_modules/resin-lint/config/.prettierrc --write \"lib/**/*.ts\" \"lib/**/*.tsx\"",
|
||||
"start": "./node_modules/.bin/electron .",
|
||||
"postshrinkwrap": "node ./scripts/clean-shrinkwrap.js",
|
||||
"configure": "node-gyp configure",
|
||||
@@ -35,6 +36,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"platformSpecificDependencies": [
|
||||
"fsevents",
|
||||
"pigpio",
|
||||
"winusb-driver-generator"
|
||||
],
|
||||
"dependencies": {
|
||||
@@ -62,13 +64,14 @@
|
||||
"nan": "^2.9.2",
|
||||
"node-ipc": "^9.1.1",
|
||||
"path-is-inside": "^1.0.2",
|
||||
"pigpio": "^1.2.3",
|
||||
"pretty-bytes": "^1.0.4",
|
||||
"prop-types": "^15.5.9",
|
||||
"react": "^16.8.5",
|
||||
"react-dom": "^16.8.5",
|
||||
"react2angular": "^4.0.2",
|
||||
"redux": "^3.5.2",
|
||||
"rendition": "^8.7.2",
|
||||
"rendition": "^10.1.0",
|
||||
"request": "^2.81.0",
|
||||
"resin-corvus": "^2.0.3",
|
||||
"roboto-fontface": "^0.9.0",
|
||||
@@ -85,7 +88,14 @@
|
||||
"@babel/plugin-proposal-function-bind": "^7.2.0",
|
||||
"@babel/preset-env": "^7.2.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@types/debug": "^4.1.4",
|
||||
"@types/node": "^10.14.9",
|
||||
"@types/pigpio": "^1.2.1",
|
||||
"@types/prop-types": "^15.7.1",
|
||||
"@types/react": "^16.8.22",
|
||||
"@types/react-dom": "^16.8.4",
|
||||
"@types/request": "^2.48.1",
|
||||
"@types/tmp": "^0.1.0",
|
||||
"acorn": "^6.0.5",
|
||||
"angular-mocks": "1.7.6",
|
||||
"babel-loader": "^8.0.4",
|
||||
@@ -111,10 +121,15 @@
|
||||
"node-sass": "^4.7.2",
|
||||
"omit-deep-lodash": "1.1.4",
|
||||
"pkg": "^4.3.0",
|
||||
"resin-lint": "^3.0.4",
|
||||
"sass-lint": "^1.12.1",
|
||||
"simple-progress-webpack-plugin": "^1.1.2",
|
||||
"spectron": "^5.0.0",
|
||||
"webpack": "^4.27.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"ts-loader": "^6.0.2",
|
||||
"ts-node": "^8.3.0",
|
||||
"typescript": "^3.5.1",
|
||||
"webpack": "^4.31.0",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"webpack-node-externals": "^1.7.2"
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@ const chalk = require('chalk')
|
||||
const path = require('path')
|
||||
const _ = require('lodash')
|
||||
const angularValidate = require('html-angular-validate')
|
||||
const EXIT_CODES = require('../lib/shared/exit-codes')
|
||||
const EXIT_CODES = require('../lib/gui/app/modules/exit-codes')
|
||||
const PROJECT_ROOT = path.join(__dirname, '..')
|
||||
const FILENAME = path.relative(PROJECT_ROOT, __filename)
|
||||
|
||||
|
Submodule scripts/resin updated: 1b5bb595fe...022f4509c5
@@ -20,7 +20,8 @@ const _ = require('lodash')
|
||||
const m = require('mochainon')
|
||||
const angular = require('angular')
|
||||
require('angular-mocks')
|
||||
const utils = require('../../../lib/shared/utils')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const utils = require('../../../lib/gui/app/modules/utils')
|
||||
|
||||
describe('Browser: DriveSelector', function () {
|
||||
beforeEach(angular.mock.module(
|
||||
|
@@ -20,7 +20,7 @@ const m = require('mochainon')
|
||||
const path = require('path')
|
||||
const availableDrives = require('../../../lib/gui/app/models/available-drives')
|
||||
const selectionState = require('../../../lib/gui/app/models/selection-state')
|
||||
const constraints = require('../../../lib/shared/drive-constraints')
|
||||
const constraints = require('../../../lib/gui/app/modules/drive-constraints')
|
||||
|
||||
describe('Model: availableDrives', function () {
|
||||
describe('availableDrives', function () {
|
||||
|
@@ -18,8 +18,9 @@
|
||||
|
||||
const m = require('mochainon')
|
||||
const _ = require('lodash')
|
||||
const Bluebird = require('bluebird')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../../../lib/gui/app/models/settings')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const localSettings = require('../../../lib/gui/app/models/local-settings')
|
||||
|
||||
describe('Browser: settings', function () {
|
||||
@@ -72,62 +73,6 @@ describe('Browser: settings', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('.assign()', function () {
|
||||
it('should throw if no settings', function (done) {
|
||||
settings.assign().asCallback((error) => {
|
||||
m.chai.expect(error).to.be.an.instanceof(Error)
|
||||
m.chai.expect(error.message).to.equal('Missing settings')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not override all settings', function () {
|
||||
return settings.assign({
|
||||
foo: 'bar',
|
||||
bar: 'baz'
|
||||
}).then(() => {
|
||||
m.chai.expect(settings.getAll()).to.deep.equal(_.assign({}, DEFAULT_SETTINGS, {
|
||||
foo: 'bar',
|
||||
bar: 'baz'
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
it('should store the settings to the local machine', function () {
|
||||
return localSettings.readAll().then((data) => {
|
||||
m.chai.expect(data.foo).to.be.undefined
|
||||
m.chai.expect(data.bar).to.be.undefined
|
||||
|
||||
return settings.assign({
|
||||
foo: 'bar',
|
||||
bar: 'baz'
|
||||
})
|
||||
}).then(localSettings.readAll).then((data) => {
|
||||
m.chai.expect(data.foo).to.equal('bar')
|
||||
m.chai.expect(data.bar).to.equal('baz')
|
||||
})
|
||||
})
|
||||
|
||||
it('should not change the application state if storing to the local machine results in an error', function (done) {
|
||||
settings.set('foo', 'bar').then(() => {
|
||||
m.chai.expect(settings.get('foo')).to.equal('bar')
|
||||
|
||||
const localSettingsWriteAllStub = m.sinon.stub(localSettings, 'writeAll')
|
||||
localSettingsWriteAllStub.returns(Bluebird.reject(new Error('localSettings error')))
|
||||
|
||||
settings.assign({
|
||||
foo: 'baz'
|
||||
}).asCallback((error) => {
|
||||
m.chai.expect(error).to.be.an.instanceof(Error)
|
||||
m.chai.expect(error.message).to.equal('localSettings error')
|
||||
localSettingsWriteAllStub.restore()
|
||||
m.chai.expect(settings.get('foo')).to.equal('bar')
|
||||
done()
|
||||
})
|
||||
}).catch(done)
|
||||
})
|
||||
})
|
||||
|
||||
describe('.load()', function () {
|
||||
it('should extend the application state with the local settings content', function () {
|
||||
const object = {
|
||||
@@ -160,28 +105,24 @@ describe('Browser: settings', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should reject if no key', function (done) {
|
||||
settings.set(null, true).asCallback((error) => {
|
||||
it('should reject if no key', async () => {
|
||||
try {
|
||||
await settings.set(null, true)
|
||||
m.chai.expect(true).to.be.false
|
||||
} catch (error) {
|
||||
m.chai.expect(error).to.be.an.instanceof(Error)
|
||||
m.chai.expect(error.message).to.equal('Missing setting key')
|
||||
done()
|
||||
})
|
||||
m.chai.expect(error.message).to.equal('Invalid setting key: null')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw if key is not a string', function (done) {
|
||||
settings.set(1234, true).asCallback((error) => {
|
||||
it('should throw if key is not a string', async () => {
|
||||
try {
|
||||
await settings.set(1234, true)
|
||||
m.chai.expect(true).to.be.false
|
||||
} catch (error) {
|
||||
m.chai.expect(error).to.be.an.instanceof(Error)
|
||||
m.chai.expect(error.message).to.equal('Invalid setting key: 1234')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw if setting an array', function (done) {
|
||||
settings.assign([ 1, 2, 3 ]).asCallback((error) => {
|
||||
m.chai.expect(error).to.be.an.instanceof(Error)
|
||||
m.chai.expect(error.message).to.equal('Settings must be an object')
|
||||
done()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('should set the key to undefined if no value', function () {
|
||||
@@ -202,21 +143,22 @@ describe('Browser: settings', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should not change the application state if storing to the local machine results in an error', function (done) {
|
||||
settings.set('foo', 'bar').then(() => {
|
||||
it('should not change the application state if storing to the local machine results in an error', async () => {
|
||||
await settings.set('foo', 'bar')
|
||||
m.chai.expect(settings.get('foo')).to.equal('bar')
|
||||
|
||||
const localSettingsWriteAllStub = m.sinon.stub(localSettings, 'writeAll')
|
||||
localSettingsWriteAllStub.returns(Promise.reject(new Error('localSettings error')))
|
||||
|
||||
try {
|
||||
await settings.set('foo', 'baz')
|
||||
m.chai.expect(true).to.be.false
|
||||
} catch (error) {
|
||||
m.chai.expect(error).to.be.an.instanceof(Error)
|
||||
m.chai.expect(error.message).to.equal('localSettings error')
|
||||
localSettingsWriteAllStub.restore()
|
||||
m.chai.expect(settings.get('foo')).to.equal('bar')
|
||||
|
||||
const localSettingsWriteAllStub = m.sinon.stub(localSettings, 'writeAll')
|
||||
localSettingsWriteAllStub.returns(Bluebird.reject(new Error('localSettings error')))
|
||||
|
||||
settings.set('foo', 'baz').asCallback((error) => {
|
||||
m.chai.expect(error).to.be.an.instanceof(Error)
|
||||
m.chai.expect(error.message).to.equal('localSettings error')
|
||||
localSettingsWriteAllStub.restore()
|
||||
m.chai.expect(settings.get('foo')).to.equal('bar')
|
||||
done()
|
||||
})
|
||||
}).catch(done)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
@@ -18,7 +18,7 @@
|
||||
|
||||
const m = require('mochainon')
|
||||
const ipc = require('node-ipc')
|
||||
require('../../../lib/gui/modules/child-writer')
|
||||
require('../../../lib/gui/app/modules/child-writer')
|
||||
|
||||
describe('Browser: childWriter', function () {
|
||||
it('should have the ipc config set to silent', function () {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
const m = require('mochainon')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../../../lib/gui/app/models/settings')
|
||||
const progressStatus = require('../../../lib/gui/app/modules/progress-status')
|
||||
|
||||
|
@@ -20,7 +20,7 @@ const m = require('mochainon')
|
||||
const _ = require('lodash')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const supportedFormats = require('../../../lib/shared/supported-formats')
|
||||
const supportedFormats = require('../../../lib/gui/app/modules/supported-formats')
|
||||
const angular = require('angular')
|
||||
const flashState = require('../../../lib/gui/app/models/flash-state')
|
||||
const availableDrives = require('../../../lib/gui/app/models/available-drives')
|
||||
|
@@ -19,7 +19,7 @@
|
||||
const m = require('mochainon')
|
||||
const angular = require('angular')
|
||||
require('angular-mocks')
|
||||
const units = require('../../../lib/shared/units')
|
||||
const units = require('../../../lib/gui/app/modules/units')
|
||||
|
||||
describe('Browser: ByteSize', function () {
|
||||
beforeEach(angular.mock.module(
|
||||
@@ -33,7 +33,7 @@ describe('Browser: ByteSize', function () {
|
||||
closestUnitFilter = _closestUnitFilter_
|
||||
}))
|
||||
|
||||
it('should expose lib/shared/units.js bytesToGigabytes()', function () {
|
||||
it('should expose lib/gui/app/modules/units.js bytesToGigabytes()', function () {
|
||||
m.chai.expect(closestUnitFilter).to.equal(units.bytesToClosestUnit)
|
||||
})
|
||||
})
|
||||
|
@@ -19,8 +19,8 @@
|
||||
const m = require('mochainon')
|
||||
const _ = require('lodash')
|
||||
const path = require('path')
|
||||
const constraints = require('../../lib/shared/drive-constraints')
|
||||
const messages = require('../../lib/shared/messages')
|
||||
const constraints = require('../../lib/gui/app/modules/drive-constraints')
|
||||
const messages = require('../../lib/gui/app/modules/messages')
|
||||
|
||||
describe('Shared: DriveConstraints', function () {
|
||||
describe('.isDriveLocked()', function () {
|
||||
|
@@ -18,7 +18,8 @@
|
||||
|
||||
const m = require('mochainon')
|
||||
const _ = require('lodash')
|
||||
const errors = require('../../lib/shared/errors')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const errors = require('../../lib/gui/app/modules/errors')
|
||||
|
||||
describe('Shared: Errors', function () {
|
||||
describe('.HUMAN_FRIENDLY', function () {
|
||||
@@ -64,16 +65,6 @@ describe('Shared: Errors', function () {
|
||||
m.chai.expect(errors.getTitle(error)).to.equal('An error ocurred')
|
||||
})
|
||||
|
||||
it('should return a generic error message if the error is undefined', function () {
|
||||
const error = undefined
|
||||
m.chai.expect(errors.getTitle(error)).to.equal('An error ocurred')
|
||||
})
|
||||
|
||||
it('should return a generic error message if the error is null', function () {
|
||||
const error = null
|
||||
m.chai.expect(errors.getTitle(error)).to.equal('An error ocurred')
|
||||
})
|
||||
|
||||
it('should return the error message', function () {
|
||||
const error = new Error('This is an error')
|
||||
m.chai.expect(errors.getTitle(error)).to.equal('This is an error')
|
||||
@@ -325,54 +316,21 @@ describe('Shared: Errors', function () {
|
||||
m.chai.expect(errors.getDescription(error)).to.equal('Memory error')
|
||||
})
|
||||
|
||||
describe('given userFriendlyDescriptionsOnly is false', function () {
|
||||
it('should return the stack for a basic error', function () {
|
||||
const error = new Error('Foo')
|
||||
m.chai.expect(errors.getDescription(error, {
|
||||
userFriendlyDescriptionsOnly: false
|
||||
})).to.equal(error.stack)
|
||||
})
|
||||
|
||||
it('should return the stack if the description is an empty string', function () {
|
||||
const error = new Error('Foo')
|
||||
error.description = ''
|
||||
m.chai.expect(errors.getDescription(error, {
|
||||
userFriendlyDescriptionsOnly: false
|
||||
})).to.equal(error.stack)
|
||||
})
|
||||
|
||||
it('should return the stack if the description is a blank string', function () {
|
||||
const error = new Error('Foo')
|
||||
error.description = ' '
|
||||
m.chai.expect(errors.getDescription(error, {
|
||||
userFriendlyDescriptionsOnly: false
|
||||
})).to.equal(error.stack)
|
||||
})
|
||||
it('should return the stack for a basic error', function () {
|
||||
const error = new Error('Foo')
|
||||
m.chai.expect(errors.getDescription(error)).to.equal(error.stack)
|
||||
})
|
||||
|
||||
describe('given userFriendlyDescriptionsOnly is true', function () {
|
||||
it('should return an empty string for a basic error', function () {
|
||||
const error = new Error('Foo')
|
||||
m.chai.expect(errors.getDescription(error, {
|
||||
userFriendlyDescriptionsOnly: true
|
||||
})).to.equal('')
|
||||
})
|
||||
it('should return the stack if the description is an empty string', function () {
|
||||
const error = new Error('Foo')
|
||||
error.description = ''
|
||||
m.chai.expect(errors.getDescription(error)).to.equal(error.stack)
|
||||
})
|
||||
|
||||
it('should return an empty string if the description is an empty string', function () {
|
||||
const error = new Error('Foo')
|
||||
error.description = ''
|
||||
m.chai.expect(errors.getDescription(error, {
|
||||
userFriendlyDescriptionsOnly: true
|
||||
})).to.equal('')
|
||||
})
|
||||
|
||||
it('should return an empty string if the description is a blank string', function () {
|
||||
const error = new Error('Foo')
|
||||
error.description = ' '
|
||||
m.chai.expect(errors.getDescription(error, {
|
||||
userFriendlyDescriptionsOnly: true
|
||||
})).to.equal('')
|
||||
})
|
||||
it('should return the stack if the description is a blank string', function () {
|
||||
const error = new Error('Foo')
|
||||
error.description = ' '
|
||||
m.chai.expect(errors.getDescription(error)).to.equal(error.stack)
|
||||
})
|
||||
})
|
||||
|
||||
|
@@ -18,7 +18,7 @@
|
||||
|
||||
const m = require('mochainon')
|
||||
const _ = require('lodash')
|
||||
const fileExtensions = require('../../lib/shared/file-extensions')
|
||||
const fileExtensions = require('../../lib/gui/app/modules/file-extensions')
|
||||
|
||||
describe('Shared: fileExtensions', function () {
|
||||
describe('.getFileExtensions()', function () {
|
||||
|
@@ -18,7 +18,7 @@
|
||||
|
||||
const m = require('mochainon')
|
||||
const _ = require('lodash')
|
||||
const messages = require('../../lib/shared/messages')
|
||||
const messages = require('../../lib/gui/app/modules/messages')
|
||||
|
||||
describe('Shared: Messages', function () {
|
||||
beforeEach(function () {
|
||||
|
@@ -20,7 +20,7 @@
|
||||
|
||||
const m = require('mochainon')
|
||||
const os = require('os')
|
||||
const permissions = require('../../lib/shared/permissions')
|
||||
const permissions = require('../../lib/gui/app/modules/permissions')
|
||||
|
||||
describe('Shared: permissions', function () {
|
||||
describe('.createLaunchScript()', function () {
|
||||
|
@@ -18,7 +18,7 @@
|
||||
|
||||
const m = require('mochainon')
|
||||
const _ = require('lodash')
|
||||
const supportedFormats = require('../../lib/shared/supported-formats')
|
||||
const supportedFormats = require('../../lib/gui/app/modules/supported-formats')
|
||||
|
||||
describe('Shared: SupportedFormats', function () {
|
||||
describe('.getCompressedExtensions()', function () {
|
||||
|
@@ -17,7 +17,7 @@
|
||||
'use strict'
|
||||
|
||||
const m = require('mochainon')
|
||||
const units = require('../../lib/shared/units')
|
||||
const units = require('../../lib/gui/app/modules/units')
|
||||
|
||||
describe('Shared: Units', function () {
|
||||
describe('.bytesToClosestUnit()', function () {
|
||||
|
@@ -18,7 +18,8 @@
|
||||
|
||||
const _ = require('lodash')
|
||||
const m = require('mochainon')
|
||||
const utils = require('../../lib/shared/utils')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const utils = require('../../lib/gui/app/modules/utils')
|
||||
|
||||
describe('Shared: Utils', function () {
|
||||
describe('.isValidPercentage()', function () {
|
||||
|
@@ -19,7 +19,7 @@
|
||||
const Bluebird = require('bluebird')
|
||||
const spectron = require('spectron')
|
||||
const m = require('mochainon')
|
||||
const EXIT_CODES = require('../../lib/shared/exit-codes')
|
||||
const EXIT_CODES = require('../../lib/gui/app/modules/exit-codes')
|
||||
const entrypoint = process.env.ETCHER_SPECTRON_ENTRYPOINT
|
||||
|
||||
if (!entrypoint) {
|
||||
|
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"noImplicitAny": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"strictNullChecks": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true,
|
||||
"moduleResolution": "node",
|
||||
"module": "commonjs",
|
||||
"target": "es2017",
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": [
|
||||
"lib/**/*.ts",
|
||||
"node_modules/electron/**/*.d.ts",
|
||||
"typings/**/*.d.ts"
|
||||
]
|
||||
}
|
@@ -27,7 +27,6 @@ const commonConfig = {
|
||||
// Minification breaks angular.
|
||||
minimize: false
|
||||
},
|
||||
target: 'electron-main',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
@@ -51,11 +50,16 @@ const commonConfig = {
|
||||
use: {
|
||||
loader: 'html-loader'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: [ '.js', '.jsx', '.json' ]
|
||||
extensions: [ '.js', '.jsx', '.json', '.ts', '.tsx' ]
|
||||
},
|
||||
plugins: [
|
||||
new SimpleProgressWebpackPlugin({
|
||||
@@ -64,91 +68,92 @@ const commonConfig = {
|
||||
]
|
||||
}
|
||||
|
||||
const guiConfig = _.assign({
|
||||
node: {
|
||||
__dirname: true,
|
||||
__filename: true
|
||||
},
|
||||
externals: [
|
||||
nodeExternals(),
|
||||
(context, request, callback) => {
|
||||
// eslint-disable-next-line lodash/prefer-lodash-method
|
||||
const absoluteContext = path.resolve(context)
|
||||
const absoluteNodeModules = path.resolve('node_modules')
|
||||
const guiConfig = _.assign(
|
||||
{},
|
||||
commonConfig,
|
||||
{
|
||||
node: {
|
||||
__dirname: true,
|
||||
__filename: true
|
||||
},
|
||||
target: 'electron-renderer',
|
||||
externals: [
|
||||
nodeExternals(),
|
||||
(context, request, callback) => {
|
||||
// eslint-disable-next-line lodash/prefer-lodash-method
|
||||
const absoluteContext = path.resolve(context)
|
||||
const absoluteNodeModules = path.resolve('node_modules')
|
||||
|
||||
// We shouldn't rewrite any node_modules import paths
|
||||
// eslint-disable-next-line lodash/prefer-lodash-method
|
||||
if (!path.relative(absoluteNodeModules, absoluteContext).startsWith('..')) {
|
||||
return callback()
|
||||
}
|
||||
|
||||
// We shouldn't rewrite any node_modules import paths
|
||||
// eslint-disable-next-line lodash/prefer-lodash-method
|
||||
if (!path.relative(absoluteNodeModules, absoluteContext).startsWith('..')) {
|
||||
return callback()
|
||||
}
|
||||
|
||||
// We want to keep the SDK code outside the GUI bundle.
|
||||
// This piece of code allows us to run the GUI directly
|
||||
// on the tree (for testing purposes) or inside a generated
|
||||
// bundle (for production purposes), by translating
|
||||
// relative require paths within the bundle.
|
||||
if (/\/(sdk|shared)/i.test(request) || /package\.json$/.test(request)) {
|
||||
const output = path.join(__dirname, 'generated')
|
||||
const dirname = path.join(context, request)
|
||||
const relative = path.relative(output, dirname)
|
||||
return callback(null, `commonjs ${path.join('..', '..', relative)}`)
|
||||
}
|
||||
|
||||
return callback()
|
||||
],
|
||||
entry: {
|
||||
gui: path.join(__dirname, 'lib', 'gui', 'app', 'app.js')
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, 'generated'),
|
||||
filename: '[name].js'
|
||||
}
|
||||
],
|
||||
entry: {
|
||||
gui: path.join(__dirname, 'lib', 'gui', 'app', 'app.js')
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, 'generated'),
|
||||
filename: '[name].js'
|
||||
}
|
||||
}, commonConfig)
|
||||
)
|
||||
|
||||
const etcherConfig = _.assign({
|
||||
node: {
|
||||
__dirname: false,
|
||||
__filename: true
|
||||
},
|
||||
externals: [
|
||||
nodeExternals(),
|
||||
(context, request, callback) => {
|
||||
// eslint-disable-next-line lodash/prefer-lodash-method
|
||||
const absoluteContext = path.resolve(context)
|
||||
const absoluteNodeModules = path.resolve('node_modules')
|
||||
const etcherConfig = _.assign(
|
||||
{},
|
||||
commonConfig,
|
||||
{
|
||||
node: {
|
||||
__dirname: false,
|
||||
__filename: true
|
||||
},
|
||||
target: 'electron-main',
|
||||
externals: [
|
||||
nodeExternals(),
|
||||
(context, request, callback) => {
|
||||
// eslint-disable-next-line lodash/prefer-lodash-method
|
||||
const absoluteContext = path.resolve(context)
|
||||
const absoluteNodeModules = path.resolve('node_modules')
|
||||
|
||||
// We shouldn't rewrite any node_modules import paths
|
||||
// eslint-disable-next-line lodash/prefer-lodash-method
|
||||
if (!path.relative(absoluteNodeModules, absoluteContext).startsWith('..')) {
|
||||
return callback()
|
||||
}
|
||||
|
||||
// We shouldn't rewrite any node_modules import paths
|
||||
// eslint-disable-next-line lodash/prefer-lodash-method
|
||||
if (!path.relative(absoluteNodeModules, absoluteContext).startsWith('..')) {
|
||||
return callback()
|
||||
}
|
||||
|
||||
// We want to keep the SDK code outside the GUI bundle.
|
||||
// This piece of code allows us to run the GUI directly
|
||||
// on the tree (for testing purposes) or inside a generated
|
||||
// bundle (for production purposes), by translating
|
||||
// relative require paths within the bundle.
|
||||
if (/\/shared/i.test(request) || /package\.json$/.test(request)) {
|
||||
const output = path.join(__dirname, 'generated')
|
||||
const dirname = path.join(context, request)
|
||||
const relative = path.relative(output, dirname)
|
||||
return callback(null, `commonjs ${path.join('..', 'lib', relative)}`)
|
||||
}
|
||||
|
||||
return callback()
|
||||
],
|
||||
entry: {
|
||||
etcher: path.join(__dirname, 'lib', 'gui', 'etcher.js')
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, 'generated'),
|
||||
filename: '[name].js'
|
||||
}
|
||||
],
|
||||
entry: {
|
||||
etcher: path.join(__dirname, 'lib', 'gui', 'etcher.js')
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, 'generated'),
|
||||
filename: '[name].js'
|
||||
}
|
||||
}, commonConfig)
|
||||
)
|
||||
|
||||
const childWriterConfig = _.assign(
|
||||
{},
|
||||
etcherConfig,
|
||||
{
|
||||
entry: {
|
||||
etcher: path.join(__dirname, 'lib', 'gui', 'app', 'modules', 'child-writer.js')
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, 'generated'),
|
||||
filename: 'child-writer.js'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
module.exports = [
|
||||
guiConfig,
|
||||
etcherConfig
|
||||
etcherConfig,
|
||||
childWriterConfig
|
||||
]
|
||||
|
Reference in New Issue
Block a user