Compare commits

...

41 Commits

Author SHA1 Message Date
Alexis Svinartchouk
868a35337c wip 2019-07-16 13:17:46 +02:00
Alexis Svinartchouk
1398ca2931 wip 2019-07-12 18:59:06 +02:00
Alexis Svinartchouk
96c865f14a wip 2019-07-10 18:44:30 +02:00
Alexis Svinartchouk
6dbd425e89 wip 2019-07-04 19:35:52 +02:00
Alexis Svinartchouk
5b2769d0e9 Sleep button on main screen for etcher-pro
Change-type: patch
2019-07-01 13:10:46 +02:00
Alexis Svinartchouk
5f38cca60c Led module prototype
Change-type: patch
2019-07-01 13:10:46 +02:00
Alexis Svinartchouk
78cebdb7a4 Change setting enableScreensaver -> screensaverDelay
Change-type: patch
2019-06-28 15:05:31 +02:00
Alexis Svinartchouk
a6aedab0a0 Add / remove listeners and timeouts when the sceensaver setting changes
Change-type: patch
Changelog-entry: Add / remove listeners and timeouts when the sceensaver setting changes
2019-06-28 15:05:31 +02:00
Alexis Svinartchouk
4aeccbe963 Don't use settings.getDefaults() in etcher.js
Change-type: patch
2019-06-28 15:05:31 +02:00
Alexis Svinartchouk
126b3fbb40 Indent typescript files with tabs
Change-type: patch
Changelog-entry: Indent typescript files with tabs
2019-06-28 15:05:31 +02:00
Alexis Svinartchouk
9ea8a6134e Remove unused settings.assign function
Change-type: patch
Changelog-entry: Remove unused settings.assign function
2019-06-28 15:05:31 +02:00
Alexis Svinartchouk
3706770322 Convert utils, settings and errors to typescript
Change-type: patch
Changelog-entry: Convert utils, settings and errors to typescript
2019-06-28 15:05:31 +02:00
Alexis Svinartchouk
7be07bfe8c Screensaver for etcher-pro
Change-type: patch
2019-06-28 15:05:31 +02:00
Alexis Svinartchouk
791c047fa1 Simplify imports
Change-type: patch
2019-06-28 15:05:30 +02:00
Lorenzo Alberto Maria Ambrosi
35ad0340b9 Move shared folder to gui/app/modules
Change-type: patch
Changelog-entry: Move shared folder to gui/app/modules
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzoa@balena.io>
2019-06-28 15:05:30 +02:00
Alexis Svinartchouk
0a0be3a13d Allow typescript files
Change-type: patch
Changelog-entry: Allow typescript files
2019-06-28 15:05:30 +02:00
Resin CI
86238af380 v1.5.51 2019-06-28 16:04:34 +03:00
Alexis Svinartchouk
d10073a052 Merge pull request #2842 from balena-io/update-sudo-prompt
Update sudo-prompt to ^9.0.0
2019-06-28 15:02:51 +02:00
Alexis Svinartchouk
b99b0d4bf8 Update sudo-prompt to ^9.0.0
Change-type: patch
Changelog-entry: Update sudo-prompt to ^9.0.0
2019-06-28 14:00:49 +02:00
Resin CI
27b5b1bf10 v1.5.50 2019-06-14 16:43:18 +03:00
Alexis Svinartchouk
bab9069dee Merge pull request #2702 from balena-io/trim
Trim
2019-06-14 15:41:29 +02:00
Alexis Svinartchouk
52a3258814 Option for trimming ext partitions on raw images
Changelog-entry: Option for trimming ext partitions on raw images
Change-type: patch
2019-06-13 20:00:20 +02:00
Alexis Svinartchouk
da548f59d1 Replace promise chains with async/await in child-writer
Change-type: patch
2019-06-13 18:42:41 +02:00
Resin CI
ecc500907c v1.5.49 2019-06-13 19:42:18 +03:00
Alexis Svinartchouk
724dade1f6 Merge pull request #2830 from balena-io/etcher-pro
Make window size configurable
2019-06-13 18:39:30 +02:00
Alexis Svinartchouk
c5dc869c03 Make window size configurable
Change-type: patch
Changelog-entry: Make window size configurable
2019-06-13 17:23:49 +02:00
Resin CI
273f7e4535 v1.5.48 2019-06-13 17:30:09 +03:00
Alexis Svinartchouk
a58e060138 Merge pull request #2829 from balena-io/dont-elevate-when-root
Don't use sudo-prompt when already elevated
2019-06-13 16:26:48 +02:00
Alexis Svinartchouk
ef4d2fcc72 Don't use sudo-prompt when already elevated
Changelog-entry: Don't use sudo-prompt when already elevated
Change-type: patch
2019-06-13 15:22:10 +02:00
Resin CI
330c06d926 v1.5.47 2019-06-12 16:30:52 +03:00
Lorenzo Alberto Maria Ambrosi
be9c36828a Merge pull request #2795 from balena-io/bump-styled-components-system
Upgrade rendition to v8
2019-06-12 15:28:08 +02:00
Lorenzo Alberto Maria Ambrosi
17f83135c5 Rework drive-selector with react + rendition
Change-type: patch
Changelog-entry: Rework drive-selector with react + rendition
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzoa@balena.io>
2019-06-10 11:43:47 +02:00
Lorenzo Alberto Maria Ambrosi
543ba51d3c Add first rendition theme configs
Change-type: patch
Changelog-entry: Use rendition theme property for step buttons
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzoa@balena.io>
2019-06-10 11:43:47 +02:00
Lorenzo Alberto Maria Ambrosi
33df23fc8c Upgrade styled-system to v4.1.0
Change-type: patch
Changelog-entry: Upgrade styled-system to v4.1.0
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzoa@balena.io>
2019-06-10 11:43:47 +02:00
Lorenzo Alberto Maria Ambrosi
3236d6b934 Upgrade rendition to v8.7.2
Change-type: patch
Changelog-entry: Upgrade rendition to v8.7.2
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzoa@balena.io>
2019-06-10 11:43:47 +02:00
Resin CI
e0e7775367 v1.5.46 2019-06-09 17:09:46 +03:00
Alexis Svinartchouk
198679583c Merge pull request #2827 from balena-io/update-ext2fs
Update ext2fs to 1.0.29
2019-06-09 16:07:37 +02:00
Alexis Svinartchouk
6dae2a604f Update ext2fs to 1.0.29
Change-type: patch
Changelog-entry: Update ext2fs to 1.0.29
2019-06-09 14:18:17 +02:00
Resin CI
68905c6ae4 v1.5.45 2019-06-04 12:58:44 +03:00
Alexis Svinartchouk
26630c4d64 Merge pull request #2823 from balena-io/trigger-build
Empty commit to trigger build
2019-06-04 11:56:24 +02:00
Alexis Svinartchouk
d382f030f0 Empty commit to trigger build
Change-type: patch
Changelog-entry: Empty commit to trigger build
2019-06-04 10:59:01 +02:00
123 changed files with 4049 additions and 2424 deletions

View File

@@ -14,3 +14,6 @@ trim_trailing_whitespace = false
[Makefile]
indent_style = tab
[*.ts]
indent_style = tab

7
.gitattributes vendored
View File

@@ -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)
@@ -48,4 +49,10 @@ CODEOWNERS text
*.rpi-sdcard binary diff=hex
*.wic binary diff=hex
*.foo binary diff=hex
*.eot binary diff=hex
*.otf binary diff=hex
*.woff binary diff=hex
*.woff2 binary diff=hex
*.ttf binary diff=hex
xz-without-extension binary diff=hex
wmic-output.txt binary diff=hex

View File

@@ -3,6 +3,44 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
# v1.5.51
## (2019-06-28)
* Update sudo-prompt to ^9.0.0 [Alexis Svinartchouk]
# v1.5.50
## (2019-06-13)
* Option for trimming ext partitions on raw images [Alexis Svinartchouk]
# v1.5.49
## (2019-06-13)
* Make window size configurable [Alexis Svinartchouk]
# v1.5.48
## (2019-06-13)
* Don't use sudo-prompt when already elevated [Alexis Svinartchouk]
# v1.5.47
## (2019-06-10)
* Rework drive-selector with react + rendition [Lorenzo Alberto Maria Ambrosi]
* Use rendition theme property for step buttons [Lorenzo Alberto Maria Ambrosi]
* Upgrade styled-system to v4.1.0 [Lorenzo Alberto Maria Ambrosi]
* Upgrade rendition to v8.7.2 [Lorenzo Alberto Maria Ambrosi]
# v1.5.46
## (2019-06-09)
* Update ext2fs to 1.0.29 [Alexis Svinartchouk]
# v1.5.45
## (2019-06-04)
* Empty commit to trigger build [Alexis Svinartchouk]
# v1.5.44
## (2019-06-03)

View File

@@ -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"))')

View File

@@ -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

View File

@@ -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()

View File

@@ -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,

View File

@@ -0,0 +1,32 @@
/*
* 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.
*/
/**
* @module Etcher.Components.TargetSelector
*/
import * as angular from 'angular';
import { react2angular } from 'react2angular';
const MODULE_NAME = 'Etcher.Components.TargetSelector';
const SelectTargetButton = angular.module(MODULE_NAME, []);
SelectTargetButton.component(
'targetSelector',
react2angular(require('./target-selector.jsx')),
);
export = MODULE_NAME;

View File

@@ -0,0 +1,166 @@
/*
* 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.
*/
/* eslint-disable no-magic-numbers */
'use strict'
// eslint-disable-next-line no-unused-vars
const React = require('react')
const propTypes = require('prop-types')
const { default: styled } = require('styled-components')
const {
ChangeButton,
DetailsText,
StepButton,
StepNameButton,
ThemedProvider
} = require('./../../styled-components')
const { Txt } = require('rendition')
const middleEllipsis = require('./../../utils/middle-ellipsis')
const { bytesToClosestUnit } = require('./../../../../gui/app/modules/units')
const TargetDetail = styled((props) => (
<Txt.span {...props}>
</Txt.span>
)) `
float: ${({ float }) => float}
`
const TargetDisplayText = ({
description,
size,
...props
}) => {
return (
<Txt.span {...props}>
<TargetDetail
float='left'>
{description}
</TargetDetail>
<TargetDetail
float='right'
>
{size}
</TargetDetail>
</Txt.span>
)
}
const TargetSelector = (props) => {
const targets = props.selection.getSelectedDrives()
if (targets.length === 1) {
const target = targets[0]
return (
<ThemedProvider>
<StepNameButton
plain
tooltip={props.tooltip}
>
{/* eslint-disable no-magic-numbers */}
{ middleEllipsis(target.description, 20) }
</StepNameButton>
{ !props.flashing &&
<ChangeButton
plain
mb={14}
onClick={props.reselectDrive}
>
Change
</ChangeButton>
}
<DetailsText>
{ props.constraints.hasListDriveImageCompatibilityStatus(targets, props.image) &&
<Txt.span className='glyphicon glyphicon-exclamation-sign'
ml={2}
tooltip={
props.constraints.getListDriveImageCompatibilityStatuses(targets, props.image)[0].message
}
/>
}
{ bytesToClosestUnit(target.size) }
</DetailsText>
</ThemedProvider>
)
}
if (targets.length > 1) {
const targetsTemplate = []
for (const target of targets) {
targetsTemplate.push((
<DetailsText
key={target.device}
tooltip={
`${target.description} ${target.displayName} ${bytesToClosestUnit(target.size)}`
}
px={21}
>
<TargetDisplayText
description={middleEllipsis(target.description, 14)}
size={bytesToClosestUnit(target.size)}
>
</TargetDisplayText>
</DetailsText>
))
}
return (
<ThemedProvider>
<StepNameButton
plain
tooltip={props.tooltip}
>
{targets.length} Targets
</StepNameButton>
{ !props.flashing &&
<ChangeButton
plain
onClick={props.reselectDrive}
mb={14}
>
Change
</ChangeButton>
}
{targetsTemplate}
</ThemedProvider>
)
}
return (
<ThemedProvider>
<StepButton
tabindex={(targets.length > 0) ? -1 : 2 }
disabled={props.disabled}
onClick={props.openDriveSelector}
>
Select target
</StepButton>
</ThemedProvider>
)
}
TargetSelector.propTypes = {
disabled: propTypes.bool,
openDriveSelector: propTypes.func,
selection: propTypes.object,
reselectDrive: propTypes.func,
flashing: propTypes.bool,
constraints: propTypes.object,
show: propTypes.bool,
tooltip: propTypes.string
}
module.exports = TargetSelector

View File

@@ -52,7 +52,7 @@
</div>
<div class="modal-footer">
<button class="button button-primary button-block"
<button class="button button-primary"
tabindex="{{ 15 + modal.getDrives().length }}"
ng-class="{
'button-warning': modal.constraints.hasListDriveImageCompatibilityStatus(modal.state.getSelectedDrives(), modal.state.getImage())

View 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);
}
}

View 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;

View File

@@ -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 */

View File

@@ -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')
@@ -87,7 +87,7 @@ class UnstyledFileListWrap extends React.PureComponent {
render () {
return (
<Flex className={ this.props.className }
innerRef={ ::this.setScrollElem }
ref={ ::this.setScrollElem }
wrap="wrap">
{ this.props.children }
</Flex>

View File

@@ -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')
@@ -58,7 +58,7 @@ const Flex = styled.div`
overflow: ${ props => props.overflow };
`
const Header = Flex.extend`
const Header = styled(Flex) `
padding: 10px 15px 0;
border-bottom: 1px solid ${ colors.primary.faded };
@@ -67,9 +67,9 @@ const Header = Flex.extend`
}
`
const Main = Flex.extend``
const Main = styled(Flex) ``
const Footer = Flex.extend`
const Footer = styled(Flex) `
padding: 10px;
flex: 0 0 auto;
border-top: 1px solid ${ colors.primary.faded };

View File

@@ -72,7 +72,7 @@ class Crumb extends React.PureComponent {
return (
<rendition.Button
onClick={ ::this.navigate }
plaintext={ true }>
plain={ true }>
<rendition.Txt bold={ this.props.bold }>
{ middleEllipsis(this.props.dir.name, FILENAME_CHAR_LIMIT_SHORT) }
</rendition.Txt>

View File

@@ -49,7 +49,7 @@ class RecentFileLink extends React.PureComponent {
return (
<rendition.Button
onClick={ ::this.select }
plaintext={ true }>
plain={ true }>
{ middleEllipsis(file.name, FILENAME_CHAR_LIMIT_SHORT) }
</rendition.Button>
)

View File

@@ -16,10 +16,12 @@
'use strict'
// eslint-disable-next-line no-unused-vars
const React = require('react')
const PropTypes = require('prop-types')
const styled = require('styled-components').default
const { position, right } = require('styled-system')
const { BaseButton, ThemedProvider } = require('../../styled-components')
const Div = styled.div `
${position}
@@ -28,11 +30,15 @@ const Div = styled.div `
const FlashAnother = (props) => {
return (
<Div position='absolute' right='152px'>
<button className="button button-primary button-brick" onClick={props.onClick.bind(null, { preserveImage: true })}>
<b>Flash Another</b>
</button>
</Div>
<ThemedProvider>
<Div position='absolute' right='152px'>
<BaseButton
primary
onClick={props.onClick.bind(null, { preserveImage: true })}>
Flash Another
</BaseButton>
</Div>
</ThemedProvider>
)
}

View File

@@ -19,12 +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 { Provider } = require('rendition')
const shared = require('./../../../../shared/units')
const shared = require('./../../../../gui/app/modules/units')
const {
StepButton,
StepNameButton,
@@ -32,44 +32,71 @@ const {
Footer,
Underline,
DetailsText,
ChangeButton
ChangeButton,
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 (
<Provider>
<ThemedProvider>
<StepNameButton
plaintext
plain
onClick={props.showSelectedImageDetails}
tooltip={props.imageBasename}
>
{/* eslint-disable no-magic-numbers */}
{ middleEllipsis(props.imageName || props.imageBasename, 20) }
</StepNameButton>
{ !props.flashing &&
<ChangeButton
plain
mb={14}
onClick={props.deselectImage}
>
Remove
</ChangeButton>
}
<DetailsText>
{shared.bytesToClosestUnit(props.imageSize)}
</DetailsText>
{ !props.flashing &&
<ChangeButton
plaintext
onClick={props.reselectImage}
>
Change
</ChangeButton>
}
</Provider>
</ThemedProvider>
)
}
return (
<Provider>
<ThemedProvider>
<StepSelection>
<StepButton
<DropDownButton
primary
onClick={props.openImageSelector}
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
@@ -79,21 +106,23 @@ const SelectImageButton = (props) => {
</Underline>
</Footer>
</StepSelection>
</Provider>
</ThemedProvider>
)
}
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

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016 resin.io
* Copyright 2018 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -83,6 +83,7 @@
.modal-footer {
flex-grow: 0;
border: 0;
text-align: center;
}
.modal {

View File

@@ -20,7 +20,11 @@ const React = require('react')
const propTypes = require('prop-types')
const Color = require('color')
const { default: styled, keyframes } = require('styled-components')
const {
default: styled,
css,
keyframes
} = require('styled-components')
const { ProgressBar, Provider } = require('rendition')
@@ -49,6 +53,10 @@ const ProgressButtonStripes = keyframes `
}
`
const ProgressButtonStripesRule = css `
${ProgressButtonStripes} 1s linear infinite;
`
const FlashProgressBar = styled(ProgressBar) `
> div {
width: 200px;
@@ -83,7 +91,7 @@ const FlashProgressBarValidating = styled(FlashProgressBar) `
background-color: white;
animation: ${ProgressButtonStripes} 1s linear infinite;
animation: ${ProgressButtonStripesRule};
overflow: hidden;
background-size: 20px 20px;
@@ -130,7 +138,6 @@ class ProgressButton extends React.Component {
<Provider>
<StepSelection>
<StepButton
primary
onClick= { this.props.callback }
disabled= { this.props.disabled }
>

View File

@@ -28,5 +28,5 @@ const react2angular = require('react2angular').react2angular
const MODULE_NAME = 'Etcher.Components.SVGIcon'
const angularSVGIcon = angular.module(MODULE_NAME, [])
angularSVGIcon.component('svgIcon', react2angular(require('./svg-icon/svg-icon.jsx')))
angularSVGIcon.component('svgIcon', react2angular(require('./svg-icon.jsx')))
module.exports = MODULE_NAME

View File

@@ -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

View File

@@ -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

View 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];
});

View File

@@ -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);
}

View File

@@ -1,231 +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,
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)
}

View 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);
}

View File

@@ -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}`
})
}
}
}

View File

@@ -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' ])

View File

@@ -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
@@ -82,12 +83,10 @@ const terminate = (code) => {
* @example
* handleError(new Error('Something bad happened!'))
*/
const handleError = (error) => {
const handleError = async (error) => {
ipc.of[IPC_SERVER_ID].emit('error', errors.toJSON(error))
Bluebird.delay(DISCONNECT_DELAY)
.then(() => {
terminate(EXIT_CODES.GENERAL_ERROR)
})
await Bluebird.delay(DISCONNECT_DELAY)
terminate(EXIT_CODES.GENERAL_ERROR)
}
/**
@@ -95,42 +94,45 @@ const handleError = (error) => {
* @param {SourceDestination} source - source
* @param {SourceDestination[]} destinations - destinations
* @param {Boolean} verify - whether to validate the writes or not
* @param {Boolean} trim - whether to trim ext partitions before writing
* @param {Function} onProgress - function to call on progress
* @param {Function} onFail - function to call on fail
* @param {Function} onFinish - function to call on finish
* @param {Function} onError - function to call on error
* @returns {Promise<void>}
* @returns {Promise<{ bytesWritten, devices, errors} >}
*
* @example
* writeAndValidate(source, destinations, verify, onProgress, onFail, onFinish, onError)
*/
const writeAndValidate = (source, destinations, verify, onProgress, onFail, onFinish, onError) => {
return source.getInnerSource()
.then((innerSource) => {
return sdk.multiWrite.pipeSourceToDestinations(
innerSource,
destinations,
onFail,
onProgress,
verify
)
})
.then(({ failures, bytesWritten }) => {
const result = {
bytesWritten,
devices: {
failed: failures.size,
successful: destinations.length - failures.size
},
errors: []
}
for (const [ destination, error ] of failures) {
error.device = destination.drive.device
result.errors.push(error)
}
onFinish(result)
})
.catch(onError)
const writeAndValidate = async (source, destinations, verify, trim, onProgress, onFail) => {
let innerSource = await source.getInnerSource()
if (trim && (await innerSource.canRead())) {
innerSource = new sdk.sourceDestination.ConfiguredSource(
innerSource,
trim,
// Create stream from file-disk (not source stream)
true
)
}
const { failures, bytesWritten } = await sdk.multiWrite.pipeSourceToDestinations(
innerSource,
destinations,
onFail,
onProgress,
verify
)
const result = {
bytesWritten,
devices: {
failed: failures.size,
successful: destinations.length - failures.size
},
errors: []
}
for (const [ destination, error ] of failures) {
error.device = destination.drive.device
result.errors.push(error)
}
return result
}
ipc.connectTo(IPC_SERVER_ID, () => {
@@ -159,7 +161,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
terminate(EXIT_CODES.SUCCESS)
})
ipc.of[IPC_SERVER_ID].on('write', (options) => {
ipc.of[IPC_SERVER_ID].on('write', async (options) => {
/**
* @summary Progress handler
* @param {Object} state - progress state
@@ -172,52 +174,20 @@ ipc.connectTo(IPC_SERVER_ID, () => {
let exitCode = EXIT_CODES.SUCCESS
/**
* @summary Finish handler
* @param {Object} results - Flash results
* @example
* writer.on('finish', onFinish)
*/
const onFinish = (results) => {
log(`Finish: ${results.bytesWritten}`)
results.errors = _.map(results.errors, (error) => {
return errors.toJSON(error)
})
ipc.of[IPC_SERVER_ID].emit('done', { results })
Bluebird.delay(DISCONNECT_DELAY)
.then(() => {
terminate(exitCode)
})
}
/**
* @summary Abort handler
* @example
* writer.on('abort', onAbort)
*/
const onAbort = () => {
const onAbort = async () => {
log('Abort')
ipc.of[IPC_SERVER_ID].emit('abort')
Bluebird.delay(DISCONNECT_DELAY)
.then(() => {
terminate(exitCode)
})
await Bluebird.delay(DISCONNECT_DELAY)
terminate(exitCode)
}
ipc.of[IPC_SERVER_ID].on('cancel', onAbort)
/**
* @summary Error handler
* @param {Error} error - error
* @example
* writer.on('error', onError)
*/
const onError = (error) => {
log(`Error: ${error.message}`)
exitCode = EXIT_CODES.GENERAL_ERROR
ipc.of[IPC_SERVER_ID].emit('error', errors.toJSON(error))
}
/**
* @summary Failure handler (non-fatal errors)
* @param {SourceDestination} destination - destination
@@ -234,24 +204,36 @@ ipc.connectTo(IPC_SERVER_ID, () => {
}
const destinations = _.map(options.destinations, 'device')
const dests = _.map(options.destinations, (destination) => {
return new sdk.sourceDestination.BlockDevice(destination, options.unmountOnSuccess)
})
const source = new sdk.sourceDestination.File(options.imagePath, sdk.sourceDestination.File.OpenFlags.Read)
writeAndValidate(
source,
dests,
options.validateWriteOnSuccess,
onProgress,
onFail,
onFinish,
onError
)
log(`Image: ${options.imagePath}`)
log(`Devices: ${destinations.join(', ')}`)
log(`Umount on success: ${options.unmountOnSuccess}`)
log(`Validate on success: ${options.validateWriteOnSuccess}`)
log(`Trim: ${options.trim}`)
const dests = _.map(options.destinations, (destination) => {
return new sdk.sourceDestination.BlockDevice(destination, options.unmountOnSuccess)
})
const source = new sdk.sourceDestination.File(options.imagePath, sdk.sourceDestination.File.OpenFlags.Read)
try {
const results = await writeAndValidate(
source,
dests,
options.validateWriteOnSuccess,
options.trim,
onProgress,
onFail
)
log(`Finish: ${results.bytesWritten}`)
results.errors = _.map(results.errors, (error) => {
return errors.toJSON(error)
})
ipc.of[IPC_SERVER_ID].emit('done', { results })
await Bluebird.delay(DISCONNECT_DELAY)
terminate(exitCode)
} catch (error) {
log(`Error: ${error.message}`)
exitCode = EXIT_CODES.GENERAL_ERROR
ipc.of[IPC_SERVER_ID].emit('error', errors.toJSON(error))
}
})
ipc.of[IPC_SERVER_ID].on('connect', () => {

View File

@@ -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')
/**

View 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);
}

View File

@@ -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')
@@ -172,7 +174,8 @@ exports.performWrite = (image, drives, onProgress) => {
uuid: flashState.getFlashUuid(),
flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: settings.get('unmountOnSuccess'),
validateWriteOnSuccess: settings.get('validateWriteOnSuccess')
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'),
trim: settings.get('trim')
}
ipc.server.on('fail', ({ device, error }) => {
@@ -200,6 +203,7 @@ exports.performWrite = (image, drives, onProgress) => {
imagePath: image,
destinations: drives,
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'),
trim: settings.get('trim'),
unmountOnSuccess: settings.get('unmountOnSuccess')
})
})
@@ -312,6 +316,7 @@ exports.flash = (image, drives) => {
flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: settings.get('unmountOnSuccess'),
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'),
trim: settings.get('trim'),
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
}
@@ -375,6 +380,7 @@ exports.cancel = () => {
flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: settings.get('unmountOnSuccess'),
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'),
trim: settings.get('trim'),
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
status: 'cancel'

View File

@@ -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)
@@ -179,6 +181,10 @@ const elevateScriptUnix = async (path, name) => {
* });
*/
exports.elevateCommand = async (command, options) => {
if (await exports.isElevated()) {
await childProcess.execFileAsync(command[0], command.slice(1), { env: options.environment })
return { cancelled: false }
}
const isWindows = os.platform() === 'win32'
const launchScript = exports.createLaunchScript(command[0], command.slice(1), options.environment)
return Bluebird.using(tmpFileDisposer({ postfix: '.cmd' }), async ({ path }) => {

View File

@@ -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

View 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));
});
}

View File

@@ -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 */

View File

@@ -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;
}

View File

@@ -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

View File

@@ -17,6 +17,7 @@
'use strict'
const electron = require('electron')
// eslint-disable-next-line node/no-missing-require
const settings = require('../models/settings')
/**

View File

@@ -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
}

View File

@@ -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')
/**

View File

@@ -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)

View File

@@ -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) {
/**

View File

@@ -49,7 +49,6 @@
> b {
color: $palette-theme-dark-soft-foreground;
font-family: monospace;
}
}

View File

@@ -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());
}
}

View File

@@ -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')

View File

@@ -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)
}
}

View File

@@ -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 (

View File

@@ -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'),
@@ -44,6 +45,7 @@ const MainPage = angular.module(MODULE_NAME, [
require('../../components/reduced-flashing-infos'),
require('../../components/flash-another'),
require('../../components/flash-results'),
require('../../components/drive-selector'),
require('../../os/open-external/open-external'),
require('../../os/dropzone/dropzone'),

View File

@@ -46,12 +46,6 @@ svg-icon > img[disabled] {
position: relative;
}
.page-main .button-brick {
width: 200px;
height: 48px;
font-size: 16px;
}
.page-main .button-abort-write {
width: 20px;
height: 20px;
@@ -67,6 +61,7 @@ svg-icon > img[disabled] {
width: 200px;
height: 48px;
font-size: 16px;
font-weight: 300;
}
%step-border {
@@ -125,10 +120,13 @@ svg-icon > img[disabled] {
}
.page-main .button.step-footer {
font-size: 12px;
font-size: 14px;
color: $palette-theme-primary-background;
border-radius: 0;
padding: 0;
width: 100%;
font-weight: 300;
height: 21px;
}
.page-main .step-drive.glyphicon {
@@ -167,7 +165,11 @@ svg-icon > img[disabled] {
.page-main .step-size {
color: $palette-theme-dark-disabled-foreground;
margin-top: 10px;
margin: 0 0 8px 0;
font-size: 14px;
line-height: 1.5;
height: 21px;
width: 100%;
}
.page-main .step-list {

View File

@@ -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,22 +20,35 @@
<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()"
>
</image-selector>
</div>
</div>
</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>
<div class="step-border-right" ng-disabled="main.shouldFlashStepBeDisabled()" ng-hide="main.state.isFlashing() && main.isWebviewShowing"></div>
@@ -35,58 +58,20 @@
</div>
<div class="space-vertical-large">
<div ng-if="!main.selection.hasDrive() && drive.shouldShowDrivesButton()">
<div>
<button class="button button-primary button-brick"
tabindex="{{ main.selection.hasDrive() ? -1 : 2 }}"
ng-disabled="main.shouldDriveStepBeDisabled()"
ng-click="drive.openDriveSelector()">Select drive</button>
</div>
</div>
<div ng-if="main.selection.hasDrive() || !drive.shouldShowDrivesButton()">
<div class="step-selection-text"
ng-class="{
'text-disabled': main.shouldDriveStepBeDisabled()
}">
<span class="step-drive step-name"
ng-class="{
'text-warning': !main.selection.getSelectedDevices().length
}"
uib-tooltip="{{ drive.getDriveListLabel() }}">
<!-- middleEllipsis errors on undefined, therefore fallback to empty string -->
{{ drive.getDrivesTitle() | middleEllipsis:20 }}
</span>
<span class="step-drive step-warning glyphicon glyphicon-exclamation-sign"
uib-tooltip="{{ main.constraints.getListDriveImageCompatibilityStatuses(main.selection.getSelectedDrives(), main.selection.getImage())[0].message }}"
ng-show="main.constraints.hasListDriveImageCompatibilityStatus(main.selection.getSelectedDrives(), main.selection.getImage())"></span>
<button class="button button-link step-footer"
tabindex="{{ main.selection.hasDrive() ? 2 : -1 }}"
ng-hide="main.state.isFlashing() || !drive.shouldShowDrivesButton()"
ng-click="drive.reselectDrive()">Change</button>
<span
ng-if="main.selection.getSelectedDevices().length <= 1"
ng-class="{
'step-fill': !drive.shouldShowDrivesButton()
}"
class="step-drive step-size">
{{ drive.getDrivesSubtitle() }}
</span>
</div>
</div>
<div ng-if="main.selection.getSelectedDevices().length > 1"
ng-class="{
'step-fill': !drive.shouldShowDrivesButton()
}"
class="step-drive step-list">
<div ng-repeat="driveObj in drive.getMemoizedSelectedDrives()"
uib-tooltip="{{ driveObj.description }} ({{ driveObj.displayName }})">
{{ driveObj.description | middleEllipsis:14 }}
</div>
</div>
<target-selector
disabled="main.shouldDriveStepBeDisabled()"
show="!main.selection.hasDrive() && drive.shouldShowDrivesButton()"
tooltip="drive.getDriveListLabel()"
selection="main.selection"
open-drive-selector="drive.openDriveSelector"
reselect-drive="drive.reselectDrive"
flashing="main.state.isFlashing()"
constraints="main.constraints"
targets="drive.getMemoizedSelectedDrives()"
>
</target-selector>
</div>
</div>
</div>

View File

@@ -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')
}
}

View File

@@ -43,6 +43,17 @@
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox"
tabindex="8"
ng-model="settings.currentData.trim"
ng-change="settings.toggle('trim')">
<span>Trim ext{2,3,4} partitions before writing (raw images only)</span>
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox"
@@ -66,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>

View File

@@ -20,7 +20,7 @@
padding: 10px;
padding-top: 11px;
border-radius: 2px;
border-radius: 24px;
border: 0;
letter-spacing: .5px;
@@ -33,6 +33,11 @@
margin-right: 2px;
}
&.button-primary{
width: 200px;
height: 48px;
}
&[disabled] {
@extend .button-no-hover;
background-color: $palette-theme-dark-disabled-background;

View File

@@ -15,7 +15,7 @@
*/
$icon-font-path: "../../../node_modules/bootstrap-sass/assets/fonts/bootstrap/";
$font-size-base: 13px;
$font-size-base: 16px;
$cursor-disabled: initial;
$link-hover-decoration: none;
$btn-min-width: 170px;
@@ -45,46 +45,93 @@ $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: Roboto;
src: url('../../../node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff');
font-weight: 100;
font-family: 'Nunito';
src: url('Nunito-Regular.eot');
src: url('./fonts/Nunito-Regular.eot?#iefix') format('embedded-opentype'),
url('./fonts/Nunito-Regular.woff2') format('woff2'),
url('./fonts/Nunito-Regular.woff') format('woff'),
url('./fonts/Nunito-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: block;
}
@font-face {
font-family: Roboto;
src: url('../../../node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff');
font-family: 'Nunito';
src: url('Nunito-Bold.eot');
src: url('./fonts/Nunito-Bold.eot?#iefix') format('embedded-opentype'),
url('./fonts/Nunito-Bold.woff2') format('woff2'),
url('./fonts/Nunito-Bold.woff') format('woff'),
url('./fonts/Nunito-Bold.ttf') format('truetype');
font-weight: bold;
font-style: normal;
font-display: block;
}
@font-face {
font-family: 'Nunito';
src: url('Nunito-Light.eot');
src: url('./fonts/Nunito-Light.eot?#iefix') format('embedded-opentype'),
url('./fonts/Nunito-Light.woff2') format('woff2'),
url('./fonts/Nunito-Light.woff') format('woff'),
url('./fonts/Nunito-Light.ttf') format('truetype');
font-weight: 300;
font-style: normal;
font-display: block;
}
@font-face {
font-family: Roboto;
src: url('../../../node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff');
font-weight: 400;
font-family: 'CircularStd';
src: url('./fonts/CircularStd-Bold.eot');
src: url('./fonts/CircularStd-Bold.eot?#iefix') format('embedded-opentype'),
url('./fonts/CircularStd-Bold.woff2') format('woff2'),
url('./fonts/CircularStd-Bold.woff') format('woff'),
url('./fonts/CircularStd-Bold.ttf') format('truetype');
font-weight: bold;
font-style: normal;
font-display: block;
}
@font-face {
font-family: Roboto;
src: url('../../../node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff');
font-family: 'CircularStd';
src: url('./fonts/CircularStd-Book.eot');
src: url('./fonts/CircularStd-Book.eot?#iefix') format('embedded-opentype'),
url('./fonts/CircularStd-Book.woff2') format('woff2'),
url('./fonts/CircularStd-Book.woff') format('woff'),
url('./fonts/CircularStd-Book.ttf') format('truetype');
font-weight: 500;
font-style: normal;
font-display: block;
}
@font-face {
font-family: Roboto;
src: url('../../../node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff');
font-weight: 700;
font-family: 'CircularStd';
src: url('./fonts/CircularStd-Medium.eot');
src: url('./fonts/CircularStd-Medium.eot?#iefix') format('embedded-opentype'),
url('./fonts/CircularStd-Medium.woff2') format('woff2'),
url('./fonts/CircularStd-Medium.woff') format('woff'),
url('./fonts/CircularStd-Medium.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: block;
}
.circular {
font-family: 'CircularStd';
font-weight: 500;
}
.nunito {
font-family: 'Nunito';
}
body {
letter-spacing: 0.5px;
display: flex;
flex-direction: column;
font-family: 'CircularStd';
> header {
flex: 0 0 auto;
@@ -100,12 +147,6 @@ body {
}
}
body,
.tooltip,
.popover {
font-family: Roboto;
}
.section-footer-main {
display: flex;
align-items: center;
@@ -173,7 +214,6 @@ body,
.section-header {
text-align: right;
padding: 5px 8px;
font-size: 15px;
> .button {
padding-left: 3px;
@@ -199,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;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016 resin.io
* Copyright 2018 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,44 +16,79 @@
'use strict'
const styled = require('styled-components').default
const { colors } = require('./theme')
// eslint-disable-next-line no-unused-vars
const React = require('react')
const { default: styled } = require('styled-components')
const {
Button, Txt, Flex
Button,
Txt,
Flex,
Provider
} = require('rendition')
const {
space
} = require('styled-system')
const { colors } = require('./theme')
exports.StepButton = styled(Button) `
width: 200px;
height: 48px;
font-size: 16px;
margin: auto;
overflow: hidden;
const theme = {
button: {
border: {
width: '0',
radius: '24px'
},
disabled: {
opacity: 1
},
extend: () => `
width: 200px;
height: 48px;
font-size: 16px;
&:disabled {
background-color: ${colors.dark.disabled.background};
color: ${colors.dark.disabled.foreground};
opacity: 1;
&:hover {
background-color: ${colors.dark.disabled.background};
color: ${colors.dark.disabled.foreground};
}
&:disabled {
background-color: ${colors.dark.disabled.background};
color: ${colors.dark.disabled.foreground};
opacity: 1;
&:hover {
background-color: ${colors.dark.disabled.background};
color: ${colors.dark.disabled.foreground};
}
}
`
}
}
exports.ThemedProvider = (props) => (
<Provider theme={theme} {...props}>
</Provider>
)
const BaseButton = styled(Button) `
height: 48px;
`
exports.ChangeButton = styled(Button) `
exports.BaseButton = BaseButton
exports.StepButton = (props) => (
<BaseButton primary {...props}>
</BaseButton>
)
exports.ChangeButton = styled(BaseButton) `
color: ${colors.primary.background};
padding: 0;
width: 100%;
height: auto;
${space}
&:hover, &:focus, &:active {
color: ${colors.primary.background};
}
`
exports.StepNameButton = styled(Button) `
exports.StepNameButton = styled(BaseButton) `
display: flex;
justify-content: center;
align-items: center;
height: 39px;
width: 100%;
font-weight: bold;
color: ${colors.dark.foreground};
@@ -77,5 +112,5 @@ exports.Underline = styled(Txt.span) `
`
exports.DetailsText = styled(Txt.p) `
color: ${colors.dark.disabled.foreground};
margin-bottom: 10px;
margin-bottom: 0;
`

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016 resin.io
* Copyright 2018 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License"),
* you may not use this file except in compliance with the License.

View File

@@ -16,7 +16,7 @@
'use strict'
const units = require('../../../../shared/units')
const units = require('../../../../gui/app/modules/units')
module.exports = () => {
/**

View File

@@ -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
View 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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1074,7 +1074,7 @@ html {
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 13px;
font-size: 16px;
line-height: 1.42857;
color: #333333;
background-color: #fff; }
@@ -1128,8 +1128,8 @@ img {
border-radius: 50%; }
hr {
margin-top: 18px;
margin-bottom: 18px;
margin-top: 22px;
margin-bottom: 22px;
border: 0;
border-top: 1px solid #eeeeee; }
@@ -1181,8 +1181,8 @@ h1, h2, h3, h4, h5, h6,
h1, .h1,
h2, .h2,
h3, .h3 {
margin-top: 18px;
margin-bottom: 9px; }
margin-top: 22px;
margin-bottom: 11px; }
h1 small,
h1 .small, .h1 small,
.h1 .small,
@@ -1197,8 +1197,8 @@ h3, .h3 {
h4, .h4,
h5, .h5,
h6, .h6 {
margin-top: 9px;
margin-bottom: 9px; }
margin-top: 11px;
margin-bottom: 11px; }
h4 small,
h4 .small, .h4 small,
.h4 .small,
@@ -1211,38 +1211,38 @@ h6, .h6 {
font-size: 75%; }
h1, .h1 {
font-size: 33px; }
font-size: 41px; }
h2, .h2 {
font-size: 27px; }
font-size: 34px; }
h3, .h3 {
font-size: 23px; }
font-size: 28px; }
h4, .h4 {
font-size: 17px; }
font-size: 20px; }
h5, .h5 {
font-size: 13px; }
font-size: 16px; }
h6, .h6 {
font-size: 12px; }
font-size: 14px; }
p {
margin: 0 0 9px; }
margin: 0 0 11px; }
.lead {
margin-bottom: 18px;
font-size: 14px;
margin-bottom: 22px;
font-size: 18px;
font-weight: 300;
line-height: 1.4; }
@media (min-width: 768px) {
.lead {
font-size: 19.5px; } }
font-size: 24px; } }
small,
.small {
font-size: 92%; }
font-size: 87%; }
mark,
.mark {
@@ -1350,14 +1350,14 @@ a.bg-danger:focus {
background-color: #e4b9b9; }
.page-header {
padding-bottom: 8px;
margin: 36px 0 18px;
padding-bottom: 10px;
margin: 44px 0 22px;
border-bottom: 1px solid #eeeeee; }
ul,
ol {
margin-top: 0;
margin-bottom: 9px; }
margin-bottom: 11px; }
ul ul,
ul ol,
ol ul,
@@ -1379,7 +1379,7 @@ ol {
dl {
margin-top: 0;
margin-bottom: 18px; }
margin-bottom: 22px; }
dt,
dd {
@@ -1418,9 +1418,9 @@ abbr[data-original-title] {
font-size: 90%; }
blockquote {
padding: 9px 18px;
margin: 0 0 18px;
font-size: 16.25px;
padding: 11px 22px;
margin: 0 0 22px;
font-size: 20px;
border-left: 5px solid #eeeeee; }
blockquote p:last-child,
blockquote ul:last-child,
@@ -1461,7 +1461,7 @@ blockquote.pull-right {
content: "\00A0 \2014"; }
address {
margin-bottom: 18px;
margin-bottom: 22px;
font-style: normal;
line-height: 1.42857; }
@@ -1493,9 +1493,9 @@ kbd {
pre {
display: block;
padding: 8.5px;
margin: 0 0 9px;
font-size: 12px;
padding: 10.5px;
margin: 0 0 11px;
font-size: 15px;
line-height: 1.42857;
color: #333333;
word-break: break-all;
@@ -2066,7 +2066,7 @@ th {
.table {
width: 100%;
max-width: 100%;
margin-bottom: 18px; }
margin-bottom: 22px; }
.table > thead > tr > th,
.table > thead > tr > td,
.table > tbody > tr > th,
@@ -2230,7 +2230,7 @@ th {
@media screen and (max-width: 767px) {
.table-responsive {
width: 100%;
margin-bottom: 13.5px;
margin-bottom: 16.5px;
overflow-y: hidden;
-ms-overflow-style: -ms-autohiding-scrollbar;
border: 1px solid #ddd; }
@@ -2275,8 +2275,8 @@ legend {
display: block;
width: 100%;
padding: 0;
margin-bottom: 18px;
font-size: 19.5px;
margin-bottom: 22px;
font-size: 24px;
line-height: inherit;
color: #333333;
border: 0;
@@ -2328,16 +2328,16 @@ input[type="checkbox"]:focus {
output {
display: block;
padding-top: 7px;
font-size: 13px;
font-size: 16px;
line-height: 1.42857;
color: #555555; }
.form-control {
display: block;
width: 100%;
height: 32px;
height: 36px;
padding: 6px 12px;
font-size: 13px;
font-size: 16px;
line-height: 1.42857;
color: #555555;
background-color: #fff;
@@ -2380,7 +2380,7 @@ textarea.form-control {
input[type="time"].form-control,
input[type="datetime-local"].form-control,
input[type="month"].form-control {
line-height: 32px; }
line-height: 36px; }
input[type="date"].input-sm, .input-group-sm > input.form-control[type="date"],
.input-group-sm > input.input-group-addon[type="date"],
.input-group-sm > .input-group-btn > input.btn[type="date"], .input-group-sm > .input-group-btn > input.button[type="date"],
@@ -2406,7 +2406,7 @@ textarea.form-control {
.input-group-sm > .input-group-btn > input.button[type="month"],
.input-group-sm
input[type="month"] {
line-height: 30px; }
line-height: 33px; }
input[type="date"].input-lg, .input-group-lg > input.form-control[type="date"],
.input-group-lg > input.input-group-addon[type="date"],
.input-group-lg > .input-group-btn > input.btn[type="date"], .input-group-lg > .input-group-btn > input.button[type="date"],
@@ -2432,7 +2432,7 @@ textarea.form-control {
.input-group-lg > .input-group-btn > input.button[type="month"],
.input-group-lg
input[type="month"] {
line-height: 45px; } }
line-height: 49px; } }
.form-group {
margin-bottom: 15px; }
@@ -2451,7 +2451,7 @@ textarea.form-control {
cursor: initial; }
.radio label,
.checkbox label {
min-height: 18px;
min-height: 22px;
padding-left: 20px;
margin-bottom: 0;
font-weight: 400;
@@ -2491,7 +2491,7 @@ textarea.form-control {
margin-left: 10px; }
.form-control-static {
min-height: 31px;
min-height: 38px;
padding-top: 7px;
padding-bottom: 7px;
margin-bottom: 0; }
@@ -2506,17 +2506,17 @@ textarea.form-control {
.input-sm, .input-group-sm > .form-control,
.input-group-sm > .input-group-addon,
.input-group-sm > .input-group-btn > .btn, .input-group-sm > .input-group-btn > .button {
height: 30px;
height: 33px;
padding: 5px 10px;
font-size: 12px;
font-size: 14px;
line-height: 1.5;
border-radius: 3px; }
select.input-sm, .input-group-sm > select.form-control,
.input-group-sm > select.input-group-addon,
.input-group-sm > .input-group-btn > select.btn, .input-group-sm > .input-group-btn > select.button {
height: 30px;
line-height: 30px; }
height: 33px;
line-height: 33px; }
textarea.input-sm, .input-group-sm > textarea.form-control,
.input-group-sm > textarea.input-group-addon,
@@ -2529,41 +2529,41 @@ select[multiple].input-sm,
height: auto; }
.form-group-sm .form-control {
height: 30px;
height: 33px;
padding: 5px 10px;
font-size: 12px;
font-size: 14px;
line-height: 1.5;
border-radius: 3px; }
.form-group-sm select.form-control {
height: 30px;
line-height: 30px; }
height: 33px;
line-height: 33px; }
.form-group-sm textarea.form-control,
.form-group-sm select[multiple].form-control {
height: auto; }
.form-group-sm .form-control-static {
height: 30px;
min-height: 30px;
height: 33px;
min-height: 36px;
padding: 6px 10px;
font-size: 12px;
font-size: 14px;
line-height: 1.5; }
.input-lg, .input-group-lg > .form-control,
.input-group-lg > .input-group-addon,
.input-group-lg > .input-group-btn > .btn, .input-group-lg > .input-group-btn > .button {
height: 45px;
height: 49px;
padding: 10px 16px;
font-size: 17px;
font-size: 20px;
line-height: 1.33333;
border-radius: 6px; }
select.input-lg, .input-group-lg > select.form-control,
.input-group-lg > select.input-group-addon,
.input-group-lg > .input-group-btn > select.btn, .input-group-lg > .input-group-btn > select.button {
height: 45px;
line-height: 45px; }
height: 49px;
line-height: 49px; }
textarea.input-lg, .input-group-lg > textarea.form-control,
.input-group-lg > textarea.input-group-addon,
@@ -2576,31 +2576,31 @@ select[multiple].input-lg,
height: auto; }
.form-group-lg .form-control {
height: 45px;
height: 49px;
padding: 10px 16px;
font-size: 17px;
font-size: 20px;
line-height: 1.33333;
border-radius: 6px; }
.form-group-lg select.form-control {
height: 45px;
line-height: 45px; }
height: 49px;
line-height: 49px; }
.form-group-lg textarea.form-control,
.form-group-lg select[multiple].form-control {
height: auto; }
.form-group-lg .form-control-static {
height: 45px;
min-height: 35px;
height: 49px;
min-height: 42px;
padding: 11px 16px;
font-size: 17px;
font-size: 20px;
line-height: 1.33333; }
.has-feedback {
position: relative; }
.has-feedback .form-control {
padding-right: 40px; }
padding-right: 45px; }
.form-control-feedback {
position: absolute;
@@ -2608,25 +2608,25 @@ select[multiple].input-lg,
right: 0;
z-index: 2;
display: block;
width: 32px;
height: 32px;
line-height: 32px;
width: 36px;
height: 36px;
line-height: 36px;
text-align: center;
pointer-events: none; }
.input-lg + .form-control-feedback, .input-group-lg > .form-control + .form-control-feedback, .input-group-lg > .input-group-addon + .form-control-feedback, .input-group-lg > .input-group-btn > .btn + .form-control-feedback, .input-group-lg > .input-group-btn > .button + .form-control-feedback,
.input-group-lg + .form-control-feedback,
.form-group-lg .form-control + .form-control-feedback {
width: 45px;
height: 45px;
line-height: 45px; }
width: 49px;
height: 49px;
line-height: 49px; }
.input-sm + .form-control-feedback, .input-group-sm > .form-control + .form-control-feedback, .input-group-sm > .input-group-addon + .form-control-feedback, .input-group-sm > .input-group-btn > .btn + .form-control-feedback, .input-group-sm > .input-group-btn > .button + .form-control-feedback,
.input-group-sm + .form-control-feedback,
.form-group-sm .form-control + .form-control-feedback {
width: 30px;
height: 30px;
line-height: 30px; }
width: 33px;
height: 33px;
line-height: 33px; }
.has-success .help-block,
.has-success .control-label,
@@ -2716,7 +2716,7 @@ select[multiple].input-lg,
color: #a94442; }
.has-feedback label ~ .form-control-feedback {
top: 23px; }
top: 27px; }
.has-feedback label.sr-only ~ .form-control-feedback {
top: 0; }
@@ -2776,7 +2776,7 @@ select[multiple].input-lg,
.form-horizontal .radio,
.form-horizontal .checkbox {
min-height: 25px; }
min-height: 29px; }
.form-horizontal .form-group {
margin-right: -15px;
@@ -2799,12 +2799,12 @@ select[multiple].input-lg,
@media (min-width: 768px) {
.form-horizontal .form-group-lg .control-label {
padding-top: 11px;
font-size: 17px; } }
font-size: 20px; } }
@media (min-width: 768px) {
.form-horizontal .form-group-sm .control-label {
padding-top: 6px;
font-size: 12px; } }
font-size: 14px; } }
.btn, .button {
display: inline-block;
@@ -2818,7 +2818,7 @@ select[multiple].input-lg,
background-image: none;
border: 1px solid transparent;
padding: 6px 12px;
font-size: 13px;
font-size: 16px;
line-height: 1.42857;
border-radius: 4px;
-webkit-user-select: none;
@@ -3086,19 +3086,19 @@ fieldset[disabled] a.button {
.btn-lg, .btn-group-lg > .btn, .btn-group-lg > .button {
padding: 10px 16px;
font-size: 17px;
font-size: 20px;
line-height: 1.33333;
border-radius: 6px; }
.btn-sm, .btn-group-sm > .btn, .btn-group-sm > .button {
padding: 5px 10px;
font-size: 12px;
font-size: 14px;
line-height: 1.5;
border-radius: 3px; }
.btn-xs, .btn-group-xs > .btn, .btn-group-xs > .button {
padding: 1px 5px;
font-size: 12px;
font-size: 14px;
line-height: 1.5;
border-radius: 3px; }
@@ -3172,7 +3172,7 @@ tbody.collapse.in {
min-width: 160px;
padding: 5px 0;
margin: 2px 0 0;
font-size: 13px;
font-size: 16px;
text-align: left;
list-style: none;
background-color: #fff;
@@ -3187,7 +3187,7 @@ tbody.collapse.in {
left: auto; }
.dropdown-menu .divider {
height: 1px;
margin: 8px 0;
margin: 10px 0;
overflow: hidden;
background-color: #e5e5e5; }
.dropdown-menu > li > a {
@@ -3236,7 +3236,7 @@ tbody.collapse.in {
.dropdown-header {
display: block;
padding: 3px 20px;
font-size: 12px;
font-size: 14px;
line-height: 1.42857;
color: #777777;
white-space: nowrap; }
@@ -3496,7 +3496,7 @@ tbody.collapse.in {
.input-group-addon {
padding: 6px 12px;
font-size: 13px;
font-size: 16px;
font-weight: 400;
line-height: 1;
color: #555555;
@@ -3508,13 +3508,13 @@ tbody.collapse.in {
.input-group-sm > .input-group-addon,
.input-group-sm > .input-group-btn > .input-group-addon.btn, .input-group-sm > .input-group-btn > .input-group-addon.button {
padding: 5px 10px;
font-size: 12px;
font-size: 14px;
border-radius: 3px; }
.input-group-addon.input-lg,
.input-group-lg > .input-group-addon,
.input-group-lg > .input-group-btn > .input-group-addon.btn, .input-group-lg > .input-group-btn > .input-group-addon.button {
padding: 10px 16px;
font-size: 17px;
font-size: 20px;
border-radius: 6px; }
.input-group-addon input[type="radio"],
.input-group-addon input[type="checkbox"] {
@@ -3607,7 +3607,7 @@ tbody.collapse.in {
border-color: #ddd; }
.nav .nav-divider {
height: 1px;
margin: 8px 0;
margin: 10px 0;
overflow: hidden;
background-color: #e5e5e5; }
.nav > li > a > img {
@@ -3701,7 +3701,7 @@ tbody.collapse.in {
.navbar {
position: relative;
min-height: 50px;
margin-bottom: 18px;
margin-bottom: 22px;
border: 1px solid transparent; }
.navbar:before, .navbar:after {
display: table;
@@ -3806,9 +3806,9 @@ tbody.collapse.in {
.navbar-brand {
float: left;
height: 50px;
padding: 16px 15px;
font-size: 17px;
line-height: 18px; }
padding: 14px 15px;
font-size: 20px;
line-height: 22px; }
.navbar-brand:hover, .navbar-brand:focus {
text-decoration: none; }
.navbar-brand > img {
@@ -3843,11 +3843,11 @@ tbody.collapse.in {
display: none; } }
.navbar-nav {
margin: 8px -15px; }
margin: 7px -15px; }
.navbar-nav > li > a {
padding-top: 10px;
padding-bottom: 10px;
line-height: 18px; }
line-height: 22px; }
@media (max-width: 767px) {
.navbar-nav .open .dropdown-menu {
position: static;
@@ -3861,7 +3861,7 @@ tbody.collapse.in {
.navbar-nav .open .dropdown-menu .dropdown-header {
padding: 5px 15px 5px 25px; }
.navbar-nav .open .dropdown-menu > li > a {
line-height: 18px; }
line-height: 22px; }
.navbar-nav .open .dropdown-menu > li > a:hover, .navbar-nav .open .dropdown-menu > li > a:focus {
background-image: none; } }
@media (min-width: 768px) {
@@ -3871,8 +3871,8 @@ tbody.collapse.in {
.navbar-nav > li {
float: left; }
.navbar-nav > li > a {
padding-top: 16px;
padding-bottom: 16px; } }
padding-top: 14px;
padding-bottom: 14px; } }
.navbar-form {
padding: 10px 15px;
@@ -3882,8 +3882,8 @@ tbody.collapse.in {
border-bottom: 1px solid transparent;
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
margin-top: 9px;
margin-bottom: 9px; }
margin-top: 7px;
margin-bottom: 7px; }
@media (min-width: 768px) {
.navbar-form .form-group {
display: inline-block;
@@ -3951,18 +3951,18 @@ tbody.collapse.in {
border-bottom-left-radius: 0; }
.navbar-btn {
margin-top: 9px;
margin-bottom: 9px; }
margin-top: 7px;
margin-bottom: 7px; }
.navbar-btn.btn-sm, .btn-group-sm > .navbar-btn.btn, .btn-group-sm > .navbar-btn.button {
margin-top: 10px;
margin-bottom: 10px; }
margin-top: 8.5px;
margin-bottom: 8.5px; }
.navbar-btn.btn-xs, .btn-group-xs > .navbar-btn.btn, .btn-group-xs > .navbar-btn.button {
margin-top: 14px;
margin-bottom: 14px; }
.navbar-text {
margin-top: 16px;
margin-bottom: 16px; }
margin-top: 14px;
margin-bottom: 14px; }
@media (min-width: 768px) {
.navbar-text {
float: left;
@@ -4104,7 +4104,7 @@ tbody.collapse.in {
.breadcrumb {
padding: 8px 15px;
margin-bottom: 18px;
margin-bottom: 22px;
list-style: none;
background-color: #f5f5f5;
border-radius: 4px; }
@@ -4120,7 +4120,7 @@ tbody.collapse.in {
.pagination {
display: inline-block;
padding-left: 0;
margin: 18px 0;
margin: 22px 0;
border-radius: 4px; }
.pagination > li {
display: inline; }
@@ -4174,7 +4174,7 @@ tbody.collapse.in {
.pagination-lg > li > a,
.pagination-lg > li > span {
padding: 10px 16px;
font-size: 17px;
font-size: 20px;
line-height: 1.33333; }
.pagination-lg > li:first-child > a,
@@ -4190,7 +4190,7 @@ tbody.collapse.in {
.pagination-sm > li > a,
.pagination-sm > li > span {
padding: 5px 10px;
font-size: 12px;
font-size: 14px;
line-height: 1.5; }
.pagination-sm > li:first-child > a,
@@ -4205,7 +4205,7 @@ tbody.collapse.in {
.pager {
padding-left: 0;
margin: 18px 0;
margin: 22px 0;
text-align: center;
list-style: none; }
.pager:before, .pager:after {
@@ -4296,7 +4296,7 @@ a.label:hover, a.label:focus {
display: inline-block;
min-width: 10px;
padding: 3px 7px;
font-size: 12px;
font-size: 14px;
font-weight: bold;
line-height: 1;
color: #fff;
@@ -4342,7 +4342,7 @@ a.badge:hover, a.badge:focus {
color: inherit; }
.jumbotron p {
margin-bottom: 15px;
font-size: 20px;
font-size: 24px;
font-weight: 200; }
.jumbotron > hr {
border-top-color: #d5d5d5; }
@@ -4363,12 +4363,12 @@ a.badge:hover, a.badge:focus {
padding-left: 60px; }
.jumbotron h1,
.jumbotron .h1 {
font-size: 59px; } }
font-size: 72px; } }
.thumbnail {
display: block;
padding: 4px;
margin-bottom: 18px;
margin-bottom: 22px;
line-height: 1.42857;
background-color: #fff;
border: 1px solid #ddd;
@@ -4394,7 +4394,7 @@ a.thumbnail.active {
.alert {
padding: 15px;
margin-bottom: 18px;
margin-bottom: 22px;
border: 1px solid transparent;
border-radius: 4px; }
.alert h4 {
@@ -4467,8 +4467,8 @@ a.thumbnail.active {
background-position: 0 0; } }
.progress {
height: 18px;
margin-bottom: 18px;
height: 22px;
margin-bottom: 22px;
overflow: hidden;
background-color: #f5f5f5;
border-radius: 4px;
@@ -4479,8 +4479,8 @@ a.thumbnail.active {
float: left;
width: 0%;
height: 100%;
font-size: 12px;
line-height: 18px;
font-size: 14px;
line-height: 22px;
color: #fff;
text-align: center;
background-color: #337ab7;
@@ -4737,7 +4737,7 @@ button.list-group-item-danger {
line-height: 1.3; }
.panel {
margin-bottom: 18px;
margin-bottom: 22px;
background-color: #fff;
border: 1px solid transparent;
border-radius: 4px;
@@ -4763,7 +4763,7 @@ button.list-group-item-danger {
.panel-title {
margin-top: 0;
margin-bottom: 0;
font-size: 15px;
font-size: 18px;
color: inherit; }
.panel-title > a,
.panel-title > small,
@@ -4938,7 +4938,7 @@ button.list-group-item-danger {
border: 0; }
.panel-group {
margin-bottom: 18px; }
margin-bottom: 22px; }
.panel-group .panel {
margin-bottom: 0;
border-radius: 4px; }
@@ -5086,7 +5086,7 @@ button.list-group-item-danger {
.close {
float: right;
font-size: 19.5px;
font-size: 24px;
font-weight: bold;
line-height: 1;
color: #000;
@@ -5249,7 +5249,7 @@ button.close {
word-spacing: normal;
word-wrap: normal;
white-space: normal;
font-size: 12px;
font-size: 14px;
filter: alpha(opacity=0);
opacity: 0; }
.tooltip.in {
@@ -5354,7 +5354,7 @@ button.close {
word-spacing: normal;
word-wrap: normal;
white-space: normal;
font-size: 13px;
font-size: 16px;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ccc;
@@ -5438,7 +5438,7 @@ button.close {
.popover-title {
padding: 8px 14px;
margin: 0;
font-size: 13px;
font-size: 16px;
background-color: #f7f7f7;
border-bottom: 1px solid #ebebeb;
border-radius: 5px 5px 0 0; }
@@ -6029,7 +6029,7 @@ body {
.button {
padding: 10px;
padding-top: 11px;
border-radius: 2px;
border-radius: 24px;
border: 0;
letter-spacing: .5px;
outline: none;
@@ -6037,6 +6037,9 @@ body {
.button > .glyphicon, .button > .tick {
top: 2px;
margin-right: 2px; }
.button.button-primary {
width: 200px;
height: 48px; }
.button[disabled] {
background-color: #3a3c41;
color: #787c7f;
@@ -6182,7 +6185,8 @@ body {
.modal-footer {
flex-grow: 0;
border: 0; }
border: 0;
text-align: center; }
.modal {
display: flex !important;
@@ -6388,11 +6392,6 @@ svg-icon > img[disabled] {
.page-main .relative {
position: relative; }
.page-main .button-brick {
width: 200px;
height: 48px;
font-size: 16px; }
.page-main .button-abort-write {
width: 20px;
height: 20px;
@@ -6406,7 +6405,8 @@ svg-icon > img[disabled] {
.button-brick {
width: 200px;
height: 48px;
font-size: 16px; }
font-size: 16px;
font-weight: 300; }
.page-main .step-border-left, .page-main .step-border-right {
height: 2px;
@@ -6452,10 +6452,13 @@ svg-icon > img[disabled] {
padding-bottom: 2px; }
.page-main .button.step-footer {
font-size: 12px;
font-size: 14px;
color: #2297de;
border-radius: 0;
padding: 0; }
padding: 0;
width: 100%;
font-weight: 300;
height: 21px; }
.page-main .step-drive.glyphicon, .page-main .step-drive.tick {
margin-top: 1px; }
@@ -6485,7 +6488,11 @@ svg-icon > img[disabled] {
.page-main .step-size {
color: #787c7f;
margin-top: 10px; }
margin: 0 0 8px 0;
font-size: 14px;
line-height: 1.5;
height: 21px;
width: 100%; }
.page-main .step-list {
height: 80px;
@@ -6591,8 +6598,7 @@ svg-icon > img[disabled] {
.page-finish .label {
display: inline-block; }
.page-finish .label > b {
color: #ddd;
font-family: monospace; }
color: #ddd; }
.page-finish .soft {
color: #ddd; }
@@ -9855,39 +9861,76 @@ readers do not read off random characters that represent icons */
font-weight: 900; }
@font-face {
font-family: Roboto;
src: url("../../../node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff");
font-weight: 100;
font-style: normal; }
@font-face {
font-family: Roboto;
src: url("../../../node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff");
font-weight: 300;
font-style: normal; }
@font-face {
font-family: Roboto;
src: url("../../../node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff");
font-family: 'Font Awesome 5 Free';
font-style: normal;
font-weight: 400;
font-style: normal; }
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: Roboto;
src: url("../../../node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff");
font-family: 'Nunito';
src: url("Nunito-Regular.eot");
src: url("./fonts/Nunito-Regular.eot?#iefix") format("embedded-opentype"), url("./fonts/Nunito-Regular.woff2") format("woff2"), url("./fonts/Nunito-Regular.woff") format("woff"), url("./fonts/Nunito-Regular.ttf") format("truetype");
font-weight: normal;
font-style: normal;
font-display: block; }
@font-face {
font-family: 'Nunito';
src: url("Nunito-Bold.eot");
src: url("./fonts/Nunito-Bold.eot?#iefix") format("embedded-opentype"), url("./fonts/Nunito-Bold.woff2") format("woff2"), url("./fonts/Nunito-Bold.woff") format("woff"), url("./fonts/Nunito-Bold.ttf") format("truetype");
font-weight: bold;
font-style: normal;
font-display: block; }
@font-face {
font-family: 'Nunito';
src: url("Nunito-Light.eot");
src: url("./fonts/Nunito-Light.eot?#iefix") format("embedded-opentype"), url("./fonts/Nunito-Light.woff2") format("woff2"), url("./fonts/Nunito-Light.woff") format("woff"), url("./fonts/Nunito-Light.ttf") format("truetype");
font-weight: 300;
font-style: normal;
font-display: block; }
@font-face {
font-family: 'CircularStd';
src: url("./fonts/CircularStd-Bold.eot");
src: url("./fonts/CircularStd-Bold.eot?#iefix") format("embedded-opentype"), url("./fonts/CircularStd-Bold.woff2") format("woff2"), url("./fonts/CircularStd-Bold.woff") format("woff"), url("./fonts/CircularStd-Bold.ttf") format("truetype");
font-weight: bold;
font-style: normal;
font-display: block; }
@font-face {
font-family: 'CircularStd';
src: url("./fonts/CircularStd-Book.eot");
src: url("./fonts/CircularStd-Book.eot?#iefix") format("embedded-opentype"), url("./fonts/CircularStd-Book.woff2") format("woff2"), url("./fonts/CircularStd-Book.woff") format("woff"), url("./fonts/CircularStd-Book.ttf") format("truetype");
font-weight: 500;
font-style: normal; }
font-style: normal;
font-display: block; }
@font-face {
font-family: Roboto;
src: url("../../../node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff");
font-weight: 700;
font-style: normal; }
font-family: 'CircularStd';
src: url("./fonts/CircularStd-Medium.eot");
src: url("./fonts/CircularStd-Medium.eot?#iefix") format("embedded-opentype"), url("./fonts/CircularStd-Medium.woff2") format("woff2"), url("./fonts/CircularStd-Medium.woff") format("woff"), url("./fonts/CircularStd-Medium.ttf") format("truetype");
font-weight: 400;
font-style: normal;
font-display: block; }
.circular {
font-family: 'CircularStd';
font-weight: 500; }
.nunito {
font-family: 'Nunito'; }
body {
letter-spacing: 0.5px;
display: flex;
flex-direction: column; }
flex-direction: column;
font-family: 'CircularStd'; }
body > header {
flex: 0 0 auto; }
body > main {
@@ -9896,11 +9939,6 @@ body {
body > footer {
flex: 0 0 auto; }
body,
.tooltip,
.popover {
font-family: Roboto; }
.section-footer-main {
display: flex;
align-items: center;
@@ -9946,8 +9984,7 @@ body,
.section-header {
text-align: right;
padding: 5px 8px;
font-size: 15px; }
padding: 5px 8px; }
.section-header > .button {
padding-left: 3px;
padding-right: 3px; }
@@ -9966,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; }

View File

@@ -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,16 +55,20 @@ 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({
width: 800,
height: 480,
useContentSize: true,
// eslint-disable-next-line no-magic-numbers
width: parseInt(config.width, 10) || 800,
// eslint-disable-next-line no-magic-numbers
height: parseInt(config.height, 10) || 480,
frame: !config.fullscreen,
useContentSize: false,
show: false,
resizable: Boolean(config.fullscreen),
resizable: false,
maximizable: false,
fullscreen: Boolean(config.fullscreen),
fullscreenable: Boolean(config.fullscreen),
@@ -136,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')

View File

@@ -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)
}

Some files were not shown because too many files have changed in this diff Show More