Compare commits

...

77 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
Resin CI
08fca87b2f v1.5.44 2019-06-03 21:16:49 +03:00
Alexis Svinartchouk
33441a1c5c Merge pull request #2803 from balena-io/fix-windows-elevation
Fix elevation on windows when the path contains "&" or "'"
2019-06-03 20:14:45 +02:00
Alexis Svinartchouk
6d8346b13a Fix elevation on windows when the path contains "&" or "'"
Change-type: patch
Changelog-entry: Fix elevation on windows when the path contains "&" or "'"
2019-05-29 17:52:01 +02:00
Resin CI
d9b340ca45 v1.5.43 2019-05-28 21:59:10 +03:00
Lorenzo Alberto Maria Ambrosi
ebbc52ee1f Merge pull request #2806 from balena-io/rollback-update-webpack
Rollback update webpack
2019-05-28 20:57:06 +02:00
Lorenzo Alberto Maria Ambrosi
de5bee29ef Revert "Include sass in webpack configs"
This reverts commit 156c25cea1.

Change-type: patch
Changelog-entry: Revert "Include sass in webpack configs"
2019-05-28 19:34:12 +02:00
Resin CI
25f843ec0b v1.5.42 2019-05-28 17:41:14 +03:00
Lorenzo Alberto Maria Ambrosi
df600a9e14 Merge pull request #2794 from balena-io/update-webpack
Add sass-loader to webpack configs
2019-05-28 16:38:55 +02:00
Lorenzo Alberto Maria Ambrosi
156c25cea1 Include sass in webpack configs
Change-type: patch
Changelog-entry: Include sass in webpack configs
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzoa@balena.io>
2019-05-28 11:08:54 +02:00
Resin CI
f7dd04e3de v1.5.41 2019-05-27 16:59:31 +03:00
Alexis Svinartchouk
3036d86cfa Merge pull request #2801 from mhajder/patch-1
waffle.io removal and adding a link to the license
2019-05-27 15:57:19 +02:00
Mateusz Hajder
3fccd52884 waffle.io removal and adding a link to the license
Change-type: patch
Changelog-entry: waffle.io removal and adding a link to the license
2019-05-27 14:56:17 +02:00
Resin CI
00640274fc v1.5.40 2019-05-27 13:16:18 +03:00
Lorenzo Alberto Maria Ambrosi
a7e8fb98b3 Merge pull request #2799 from balena-io/combine-ia32-and-x64-2
Combine ia32 and x64 2
2019-05-27 12:14:09 +02:00
Alexis Svinartchouk
bed6643437 Remove some unused files from the packages
Change-type: patch
2019-05-24 11:26:45 +02:00
Alexis Svinartchouk
f815e8511f Build packages that support both ia32 and x64 on windows
Changelog-entry: windows installer and portable version support both ia32 and x64
Change-type: patch
2019-05-21 18:02:06 +02:00
Resin CI
6360fd42e7 v1.5.39 2019-05-14 13:27:35 +03:00
Lorenzo Alberto Maria Ambrosi
62a9656888 Merge pull request #2768 from balena-io/clean-shrinkwrap
Add clean-shrinkwrap script to postshrinkwrap step
2019-05-14 12:25:05 +02:00
Lorenzo Alberto Maria Ambrosi
ffb89c7e5b Update scripts submodule to v1.5.2
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzoa@balena.io>
2019-05-14 09:39:48 +02:00
Lorenzo Alberto Maria Ambrosi
aa52735006 Add clean-shrinkwrap script to postshrinkwrap step
Change-type: patch
Changelog-entry: Add clean-shrinkwrap script to postshrinkwrap step
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzoa@balena.io>
2019-05-14 09:39:48 +02:00
Resin CI
b65526d8ee v1.5.38 2019-05-14 01:47:31 +03:00
Carlo Maria Curinga
ea8e2999ae Merge pull request #2786 from balena-io/add-mention-to-usbboot-devices
add mention to usbboot devices support
2019-05-14 00:45:00 +02:00
Carlo Maria Curinga
0b5017f992 add mention to usbboot devices support
Change-type: patch
Changelog-entry: Add mention to usbboot compatibility
Signed-off-by: Carlo Maria Curinga carlo@balena.io
2019-05-13 23:38:13 +02:00
Resin CI
8bf1bdaa04 v1.5.37 2019-05-13 20:53:18 +03:00
Lorenzo Alberto Maria Ambrosi
01eb3b1c94 Merge pull request #2783 from balena-io/bump-react
Bump react to v16.8.5
2019-05-13 19:51:00 +02:00
Lorenzo Alberto Maria Ambrosi
3402c9f601 Bump react to v16.8.5
Change-type: patch
Changelog-entry: Bump react dependency to v16.8.5
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzoa@balena.io>
2019-05-13 15:09:23 +02:00
Resin CI
13c3518c5e v1.5.36 2019-05-13 15:35:11 +03:00
Alexis Svinartchouk
821fad27dc Merge pull request #2782 from balena-io/sdk-2.0.9
Update etcher-sdk to ^2.0.9
2019-05-13 14:32:51 +02:00
Alexis Svinartchouk
50a34e2f4c Update etcher-sdk to ^2.0.9
Changelog-entry: Update etcher-sdk to ^2.0.9
Change-type: patch
2019-05-13 12:51:39 +02:00
Resin CI
2a19b2afbe v1.5.35 2019-05-10 20:29:42 +03:00
Alexis Svinartchouk
dc92d010fb Merge pull request #2775 from balena-io/electron-3.1.9
Downgrade electron 4.1.5 -> 3.1.9
2019-05-10 19:27:31 +02:00
Alexis Svinartchouk
9cb27a616a Downgrade electron 4.1.5 -> 3.1.9
Changelog-entry: Downgrade electron 4.1.5 -> 3.1.9
Change-type: patch
2019-05-10 14:18:33 +02:00
Resin CI
518a0ca45b v1.5.34 2019-05-10 13:21:12 +03:00
Alexis Svinartchouk
3526a0e3c5 Merge pull request #2774 from balena-io/1.5.34
1.5.34
2019-05-10 12:19:17 +02:00
Alexis Svinartchouk
6386f85258 Use https url for fetching config, avoid redirection
Changelog-entry: Use https url for fetching config, avoid redirection
Change-type: patch
2019-05-09 16:01:36 +02:00
Alexis Svinartchouk
e80106d8f8 Update etcher-sdk to ^2.0.7
Changelog-entry: win32: fix running diskpart when the tmp file path contains spaces
Change-type: patch
2019-05-09 15:58:40 +02:00
131 changed files with 4689 additions and 3056 deletions

View File

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

View File

@@ -318,8 +318,6 @@ rules:
- always
prefer-const:
- error
prefer-reflect:
- error
prefer-spread:
- error
prefer-numeric-literals:

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

@@ -16,11 +16,67 @@
"appId": "io.balena.etcher",
"copyright": "Copyright 2016-2019 Balena Ltd",
"productName": "balenaEtcher",
"nodeGypRebuild": false,
"nodeGypRebuild": true,
"files": [
"!node_modules/**/*.js.map",
"!node_modules/**/*.h",
"!node_modules/**/*.hpp",
"!node_modules/**/*.cpp",
"!node_modules/**/*.md",
"!node_modules/**/*.ts",
"!node_modules/**/*.coffee",
"!node_modules/**/*.scss",
"!node_modules/**/*.less",
"!node_modules/**/*.hbs",
"!node_modules/**/*.mkd",
"!node_modules/**/LICENSE",
"!node_modules/**/LICENCE",
"!node_modules/**/license",
"!node_modules/**/License",
"!node_modules/**/LICENSE.txt",
"!node_modules/**/Makefile",
"!node_modules/**/.editorconfig",
"!node_modules/**/.babelrc",
"!node_modules/**/.prettierrc",
"!node_modules/**/.prettierrc-*",
"!node_modules/**/.eslintrc.yml",
"!node_modules/**/.eslintignore",
"!node_modules/**/.publishrc",
"!lib/gui/app",
"lib/gui/app/index.html",
"generated"
"generated",
"!node_modules/chart.js/dist/docs",
"!node_modules/ext2fs/config",
"!node_modules/ext2fs/deps",
"!node_modules/ext2fs/LICENSE",
"!node_modules/ext2fs/src",
"!node_modules/winusb-driver-generator/src",
"!node_modules/winusb-driver-generator/deps",
"!node_modules/winusb-driver-generator/ci",
"!node_modules/rendition/__screenshots__",
"!node_modules/polished/docs",
"!node_modules/mermaid/src",
"!node_modules/mermaid/dist",
"node_modules/mermaid/dist/mermaid.core.js",
"!node_modules/raven-js/src",
"!node_modules/raven-js/dist",
"node_modules/raven-js/dist/raven.js",
"!node_modules/raven-js/plugins",
"!node_modules/react-jsonschema-form/dist",
"!node_modules/xxhash/deps",
"!node_modules/xxhash/src",
"!node_modules/unzip-stream/testData*",
"!node_modules/usb",
"node_modules/usb/usb.js",
"node_modules/usb/package.json",
"node_modules/usb/build",
"node_modules/usb/src/binding",
"!node_modules/roboto-fontface/fonts",
"node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff",
"node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff",
"node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff",
"node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff",
"node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff"
],
"mac": {
"category": "public.app-category.developer-tools"

View File

@@ -3,6 +3,100 @@
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)
* Fix elevation on windows when the path contains "&" or "'" [Alexis Svinartchouk]
# v1.5.43
## (2019-05-28)
* Revert "Include sass in webpack configs" [Lorenzo Alberto Maria Ambrosi]
# v1.5.42
## (2019-05-28)
* Include sass in webpack configs [Lorenzo Alberto Maria Ambrosi]
# v1.5.41
## (2019-05-27)
* waffle.io removal and adding a link to the license [Mateusz Hajder]
# v1.5.40
## (2019-05-24)
* windows installer and portable version support both ia32 and x64 [Alexis Svinartchouk]
# v1.5.39
## (2019-05-14)
* Add clean-shrinkwrap script to postshrinkwrap step [Lorenzo Alberto Maria Ambrosi]
# v1.5.38
## (2019-05-13)
* Add mention to usbboot compatibility [Carlo Maria Curinga]
# v1.5.37
## (2019-05-13)
* Bump react dependency to v16.8.5 [Lorenzo Alberto Maria Ambrosi]
# v1.5.36
## (2019-05-13)
* Update etcher-sdk to ^2.0.9 [Alexis Svinartchouk]
# v1.5.35
## (2019-05-10)
* Downgrade electron 4.1.5 -> 3.1.9 [Alexis Svinartchouk]
# v1.5.34
## (2019-05-09)
* Use https url for fetching config, avoid redirection [Alexis Svinartchouk]
* win32: fix running diskpart when the tmp file path contains spaces [Alexis Svinartchouk]
# v1.5.33
## (2019-04-30)

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

@@ -5,13 +5,12 @@
Etcher is a powerful OS image flasher built with web technologies to ensure
flashing an SDCard or USB drive is a pleasant and safe experience. It protects
you from accidentally writing to your hard-drives, ensures every byte of data
was written correctly and much more.
was written correctly and much more. It can also flash directly Raspberry Pi devices that support the usbboot protocol
[![Current Release](https://img.shields.io/github/release/balena-io/etcher.svg?style=flat-square)](https://balena.io/etcher)
![License](https://img.shields.io/github/license/balena-io/etcher.svg?style=flat-square)
[![License](https://img.shields.io/github/license/balena-io/etcher.svg?style=flat-square)](https://github.com/balena-io/etcher/blob/master/LICENSE)
[![Dependency status](https://img.shields.io/david/balena-io/etcher.svg?style=flat-square)](https://david-dm.org/balena-io/etcher)
[![Balena.io Forums](https://img.shields.io/discourse/https/forums.balena.io/topics.svg?style=flat-square&label=balena.io%20forums)](https://forums.balena.io/c/etcher)
[![Stories in Progress](https://img.shields.io/waffle/label/balena-io/etcher/in%20progress.svg?style=flat-square)](https://waffle.io/balena-io/etcher)
***

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

@@ -1,8 +1,8 @@
appId: io.balena.etcher
copyright: Copyright 2016-2019 Balena Ltd
productName: balenaEtcher
npmRebuild: false
nodeGypRebuild: false
npmRebuild: true
nodeGypRebuild: true
publish: null
files:
- lib
@@ -37,9 +37,9 @@ nsis:
uninstallerIcon: assets/icon.ico
deleteAppDataOnUninstall: true
license: LICENSE
artifactName: "${productName}-Setup-${version}-${env.TARGET_ARCH}.${ext}"
artifactName: "${productName}-Setup-${version}.${ext}"
portable:
artifactName: "${productName}-Portable-${version}-${env.TARGET_ARCH}.${ext}"
artifactName: "${productName}-Portable-${version}.${ext}"
requestExecutionLevel: user
linux:
category: Utility

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,15 +19,17 @@
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' ])
const mixpanelToken = settings.get('analyticsMixpanelToken') ||
_.get(packageJSON, [ 'analytics', 'mixpanel', 'token' ])
const configUrl = settings.get('configUrl') || 'http://balena.io/etcher/static/config.json'
const configUrl = settings.get('configUrl') || 'https://balena.io/etcher/static/config.json'
const DEFAULT_PROBABILITY = 0.1

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

@@ -0,0 +1,224 @@
/*
* Copyright 2017 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 lodash/prefer-lodash-method,quotes,no-magic-numbers,require-jsdoc */
'use strict'
const Bluebird = require('bluebird')
const childProcess = Bluebird.promisifyAll(require('child_process'))
const fs = require('fs')
const _ = require('lodash')
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)
/**
* @summary The user id of the UNIX "superuser"
* @constant
* @type {Number}
*/
const UNIX_SUPERUSER_USER_ID = 0
/**
* @summary Check if the current process is running with elevated permissions
* @function
* @public
*
* @description
* This function has been adapted from https://github.com/sindresorhus/is-elevated,
* which was originally licensed under MIT.
*
* We're not using such module directly given that it
* contains dependencies with dynamic undeclared dependencies,
* causing a mess when trying to concatenate the code.
*
* @fulfil {Boolean} - whether the current process has elevated permissions
* @returns {Promise}
*
* @example
* permissions.isElevated().then((isElevated) => {
* if (isElevated) {
* console.log('This process has elevated permissions');
* }
* });
*/
exports.isElevated = () => {
if (os.platform() === 'win32') {
// `fltmc` is available on WinPE, XP, Vista, 7, 8, and 10
// Works even when the "Server" service is disabled
// See http://stackoverflow.com/a/28268802
return childProcess.execAsync('fltmc')
.then(_.constant(true))
.catch({
code: os.constants.errno.EPERM
}, _.constant(false))
}
return Bluebird.resolve(process.geteuid() === UNIX_SUPERUSER_USER_ID)
}
/**
* @summary Check if the current process is running with elevated permissions
* @function
* @public
*
* @description
*
* @returns {Boolean}
*
* @example
* permissions.isElevatedUnixSync()
* if (isElevated) {
* console.log('This process has elevated permissions');
* }
* });
*/
exports.isElevatedUnixSync = () => {
return (process.geteuid() === UNIX_SUPERUSER_USER_ID)
}
const escapeSh = (value) => {
// Make sure it's a string
// Replace ' -> '\'' (closing quote, escaped quote, opening quote)
// Surround with quotes
return `'${String(value).replace(/'/g, "'\\''")}'`
}
const escapeParamCmd = (value) => {
// Make sure it's a string
// Escape " -> \"
// Surround with double quotes
return `"${String(value).replace(/"/g, '\\"')}"`
}
const setEnvVarSh = (value, name) => {
return `export ${name}=${escapeSh(value)}`
}
const setEnvVarCmd = (value, name) => {
return `set "${name}=${String(value)}"`
}
// Exported for tests
exports.createLaunchScript = (command, argv, environment) => {
const isWindows = os.platform() === 'win32'
const lines = []
if (isWindows) {
// Switch to utf8
lines.push('chcp 65001')
}
const [ setEnvVarFn, escapeFn ] = isWindows ? [ setEnvVarCmd, escapeParamCmd ] : [ setEnvVarSh, escapeSh ]
lines.push(..._.map(environment, setEnvVarFn))
lines.push([ command, ...argv ].map(escapeFn).join(' '))
return lines.join(os.EOL)
}
const elevateScriptWindows = async (path) => {
// './nativeModule' imported here as it only exists on windows
// TODO: replace this with sudo-prompt once https://github.com/jorangreef/sudo-prompt/issues/96 is fixed
const nativeModule = require('./native-module')
const elevateAsync = promisify(nativeModule.load('elevator').elevate)
// '&' needs to be escaped here (but not when written to a .cmd file)
const cmd = [ 'cmd', '/c', escapeParamCmd(path).replace(/&/g, '^&') ]
const { cancelled } = await elevateAsync(cmd)
return { cancelled }
}
const elevateScriptUnix = async (path, name) => {
const cmd = [ 'sh', escapeSh(path) ].join(' ')
const [ , stderr ] = await sudoPrompt.execAsync(cmd, { name })
if (!_.isEmpty(stderr)) {
throw errors.createError({ title: stderr })
}
return { cancelled: false }
}
/**
* @summary Elevate a command
* @function
* @public
*
* @param {String[]} command - command arguments
* @param {Object} options - options
* @param {String} options.applicationName - application name
* @param {Object} options.environment - environment variables
* @fulfil {Object} - elevation results
* @returns {Promise}
*
* @example
* permissions.elevateCommand([ 'foo', 'bar' ], {
* applicationName: 'My App',
* environment: {
* FOO: 'bar'
* }
* }).then((results) => {
* if (results.cancelled) {
* console.log('Elevation has been cancelled');
* }
* });
*/
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 }) => {
await writeFileAsync(path, launchScript)
if (isWindows) {
return elevateScriptWindows(path)
}
try {
return await elevateScriptUnix(path, options.applicationName)
} catch (error) {
// We're hardcoding internal error messages declared by `sudo-prompt`.
// There doesn't seem to be a better way to handle these errors, so
// for now, we should make sure we double check if the error messages
// have changed every time we upgrade `sudo-prompt`.
console.log('error', error)
if (_.includes(error.message, 'is not in the sudoers file')) {
throw errors.createUserError({
title: "Your user doesn't have enough privileges to proceed",
description: 'This application requires sudo privileges to be able to write to drives'
})
} else if (_.startsWith(error.message, 'Command failed:')) {
throw errors.createUserError({
title: 'The elevated process died unexpectedly',
description: `The process error code was ${error.code}`
})
} else if (error.message === 'User did not grant permission.') {
return { cancelled: true }
} else if (error.message === 'No polkit authentication agent found.') {
throw errors.createUserError({
title: 'No polkit authentication agent found',
description: 'Please install a polkit authentication agent for your desktop environment of choice to continue'
})
}
throw error
}
})
}

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

225
lib/gui/app/modules/utils.ts Executable file
View File

@@ -0,0 +1,225 @@
/*
* Copyright 2017 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 Bluebird from 'bluebird';
import * as _ from 'lodash';
import * as request from 'request';
import * as tmp from 'tmp';
import { promisify } from 'util';
import * as errors from './errors';
const getAsync = promisify(request.get);
/**
* @summary Minimum percentage value
* @constant
* @public
* @type {Number}
*/
export const PERCENTAGE_MINIMUM = 0;
/**
* @summary Maximum percentage value
* @constant
* @public
* @type {Number}
*/
export const PERCENTAGE_MAXIMUM = 100;
/**
* @summary Check if a percentage is valid
* @function
* @public
*
* @param {Number} percentage - percentage
* @returns {Boolean} whether the percentage is valid
*
* @example
* if (utils.isValidPercentage(85)) {
* console.log('The percentage is valid');
* }
*/
export function isValidPercentage(percentage: number) {
return _.every([
_.isNumber(percentage),
percentage >= exports.PERCENTAGE_MINIMUM,
percentage <= exports.PERCENTAGE_MAXIMUM,
]);
}
/**
* @summary Convert a percentage to a float
* @function
* @public
*
* @param {Number} percentage - percentage
* @returns {Number} float percentage
*
* @example
* const value = utils.percentageToFloat(50);
* console.log(value);
* > 0.5
*/
export function percentageToFloat(percentage: number) {
if (!isValidPercentage(percentage)) {
throw errors.createError({
title: `Invalid percentage: ${percentage}`,
});
}
return percentage / PERCENTAGE_MAXIMUM;
}
/**
* @summary Memoize a function
* @function
* @private
*
* @description
* This workaround is needed to avoid AngularJS from getting
* caught in an infinite digest loop when using `ngRepeat`
* over a function that returns a mutable version of an
* ImmutableJS object.
*
* The problem is that every time you call `myImmutableObject.toJS()`
* you will get a new object, whose reference is different from
* the one you previously got, even if the data is exactly the same.
*
* @param {Function} func - function that returns an ImmutableJS list
* @param {Function} comparer - function to compare old and new args and state
* @returns {Function} memoized function
*
* @example
* const getList = () => {
* return Store.getState().toJS().myList;
* };
*
* const memoizedFunction = memoize(getList, angular.equals);
*/
export function memoize(
func: (...args: any[]) => any,
comparer: (a: any, b: any) => boolean,
) {
let previousTuples: any[] = [];
return (...restArgs: any[]) => {
let areArgsInTuple = false;
let state = Reflect.apply(func, this, restArgs);
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;
}
// Update the tuple state
return [oldArgs, state];
}
// Return the tuple unchanged
return [oldArgs, oldState];
});
// Add the state associated with these args to be memoized
if (!areArgsInTuple) {
previousTuples.push([restArgs, state]);
}
return state;
};
}
/**
* @summary Check if obj has one or many specific props
* @function
* @public
*
* @param {Object} obj - object
* @param {Array<String>} props - properties
*
* @returns {Boolean}
*
* @example
* const doesIt = hasProps({ foo: 'bar' }, [ 'foo' ]);
*/
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
*/
export async function getConfig(configUrl: string) {
// @ts-ignore
return (await getAsync(configUrl, { json: true })).body;
}
/**
* @summary returns { path: String, cleanup: Function }
* @function
*
* @param {Object} options - options
*
* @returns {Promise<{ path: String, cleanup: Function }>}
*
* @example
* tmpFileAsync()
* .then({ path, cleanup } => {
* console.log(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 });
}
});
});
}
/**
* @summary Disposer for tmpFileAsync, calls cleanup()
* @function
*
* @param {Object} options - options
*
* @returns {Disposer<{ path: String, cleanup: Function }>}
*
* @example
* await Bluebird.using(tmpFileDisposer(), ({ path }) => {
* console.log(path);
* })
*/
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')
/**

63
lib/gui/app/os/windows-network-drives.js Normal file → Executable file
View File

@@ -23,59 +23,10 @@ const _ = require('lodash')
const os = require('os')
const Path = require('path')
const process = require('process')
const tmp = require('tmp')
const { promisify } = require('util')
/**
* @summary returns { path: String, cleanup: Function }
* @function
*
* @returns {Promise<{ path: String, cleanup: Function }>}
*
* @example
* tmpFileAsync()
* .then({ path, cleanup } => {
* console.log(path)
* cleanup()
* });
*/
const tmpFileAsync = () => {
return new Promise((resolve, reject) => {
const options = {
// Close the file once it's created
discardDescriptor: true,
// Wmic fails with "Invalid global switch" when the "/output:" switch filename contains a dash ("-")
prefix: 'tmp'
}
tmp.file(options, (error, path, _fd, cleanup) => {
if (error) {
reject(error)
} else {
resolve({ path, cleanup })
}
})
})
}
/**
* @summary Disposer for tmpFileAsync, calls cleanup()
* @function
*
* @returns {Disposer<{ path: String, cleanup: Function }>}
*
* @example
* await Bluebird.using(tmpFileDisposer(), ({ path }) => {
* console.log(path);
* })
*/
const tmpFileDisposer = () => {
return Bluebird.resolve(tmpFileAsync())
.disposer(({ cleanup }) => {
cleanup()
})
}
// eslint-disable-next-line node/no-missing-require
const { tmpFileDisposer } = require('../../../gui/app/modules/utils')
const readFileAsync = promisify(fs.readFile)
@@ -99,7 +50,15 @@ exports.getWmicNetworkDrivesOutput = async () => {
// We could also use wmic's "/output:" switch but it doesn't work when the filename
// contains a space and the os temp dir may contain spaces ("D:\Windows Temp Files" for example).
// So we just redirect to a file and read it afterwards as we know it will be ucs2 encoded.
return Bluebird.using(tmpFileDisposer(), async ({ path }) => {
const options = {
// Close the file once it's created
discardDescriptor: true,
// Wmic fails with "Invalid global switch" when the "/output:" switch filename contains a dash ("-")
prefix: 'tmp'
}
return Bluebird.using(tmpFileDisposer(options), async ({ path }) => {
const command = [
Path.join(process.env.SystemRoot, 'System32', 'Wbem', 'wmic'),
'path',

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.

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